From 8c5c7ec8bbc8a074884ef04b566f9c0ecd6e78bb Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 12 Oct 2014 12:38:40 -0400 Subject: update docs and example images --- gerber/rs274x.py | 327 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 gerber/rs274x.py (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py new file mode 100644 index 0000000..4076f77 --- /dev/null +++ b/gerber/rs274x.py @@ -0,0 +1,327 @@ +#! /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. +""" This module provides an RS-274-X class and parser. +""" + + +import re +import json +from .gerber_statements import * +from .cam import CamFile, FileSettings + + + + +def read(filename): + """ Read data from filename and return a GerberFile + + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.rs274x.GerberFile` + A GerberFile created from the specified file. + """ + return GerberParser().parse(filename) + + +class GerberFile(CamFile): + """ A class representing a single gerber file + + The GerberFile class represents a single gerber file. + + Parameters + ---------- + statements : list + list of gerber file statements + + settings : dict + Dictionary of gerber file settings + + filename : string + Filename of the source gerber file + + Attributes + ---------- + comments: list of strings + List of comments contained in the gerber file. + + 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)) + + """ + def __init__(self, statements, settings, filename=None): + super(GerberFile, self).__init__(statements, settings, filename) + + @property + def comments(self): + return [comment.comment for comment in self.statements + if isinstance(comment, CommentStmt)] + + @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: + if stmt.x < xbounds[0]: + xbounds[0] = stmt.x + elif stmt.x > xbounds[1]: + xbounds[1] = stmt.x + if stmt.y is not None: + if stmt.y < ybounds[0]: + ybounds[0] = stmt.y + elif stmt.y > ybounds[1]: + ybounds[1] = stmt.y + 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, ctx, filename=None): + """ Generate image of layer. + + Parameters + ---------- + ctx : :class:`GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, the rendered image will be saved to `filename` + """ + ctx.set_bounds(self.bounds) + for statement in self.statements: + ctx.evaluate(statement) + if filename is not None: + 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)\*") + + REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') + QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') + + def __init__(self): + self.settings = FileSettings() + 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 + + # Region Mode + (mode, r) = self._match_one(self.REGION_MODE_STMT, line) + if mode: + yield RegionModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # Quadrant Mode + (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) + if mode: + yield QuadrantModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # 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 + self.settings.format = stmt.format + self.settings.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 LNParamStmt.from_dict(param) + else: + yield UnknownStmt(line) + did_something = True + line = r + continue + + # eof + (eof, r) = self._match_one(self.EOF_STMT, line) + if eof: + yield EofStmt() + did_something = True + line = r + continue + + if False: + print self.COORD_STMT.pattern + print self.APERTURE_STMT.pattern + print self.COMMENT_STMT.pattern + print self.EOF_STMT.pattern + for i in self.PARAM_STMT: + print i.pattern + + if line.find('*') > 0: + yield UnknownStmt(line) + oldline = line + + def _match_one(self, expr, data): + match = expr.match(data) + if match is None: + return ({}, None) + else: + return (match.groupdict(), data[match.end(0):]) + + def _match_one_from_many(self, exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) -- cgit From 6d2db67e6d0973ce26ce3a6700ca44295f73fea7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 18 Oct 2014 01:44:51 -0400 Subject: Refactor rendering --- gerber/rs274x.py | 189 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 149 insertions(+), 40 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 4076f77..39693c9 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -19,14 +19,13 @@ """ -import re +import copy import json +import re from .gerber_statements import * +from .primitives import * from .cam import CamFile, FileSettings - - - def read(filename): """ Read data from filename and return a GerberFile @@ -72,8 +71,9 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ - def __init__(self, statements, settings, filename=None): - super(GerberFile, self).__init__(statements, settings, filename) + def __init__(self, statements, settings, primitives, filename=None): + super(GerberFile, self).__init__(statements, settings, primitives, filename) + @property def comments(self): @@ -111,22 +111,7 @@ class GerberFile(CamFile): for statement in self.statements: f.write(statement.to_gerber()) - def render(self, ctx, filename=None): - """ Generate image of layer. - Parameters - ---------- - ctx : :class:`GerberContext` - GerberContext subclass used for rendering the image - - filename : string - If provided, the rendered image will be saved to `filename` - """ - ctx.set_bounds(self.bounds) - for statement in self.statements: - ctx.evaluate(statement) - if filename is not None: - ctx.dump(filename) class GerberParser(object): @@ -178,15 +163,31 @@ class GerberParser(object): def __init__(self): self.settings = FileSettings() self.statements = [] + self.primitives = [] + self.apertures = {} + self.current_region = None + self.x = 0 + self.y = 0 + + self.aperture = 0 + self.interpolation = 'linear' + self.direction = 'clockwise' + self.image_polarity = 'positive' + self.level_polarity = 'dark' + self.region_mode = 'off' + self.quadrant_mode = 'multi-quadrant' + self.step_and_repeat = (1, 1, 0, 0) + def parse(self, filename): fp = open(filename, "r") data = fp.readlines() for stmt in self._parse(data): + self.evaluate(stmt) self.statements.append(stmt) - return GerberFile(self.statements, self.settings, filename) + return GerberFile(self.statements, self.settings, self.primitives, filename) def dump_json(self): stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} @@ -218,7 +219,7 @@ class GerberParser(object): did_something = False # Region Mode - (mode, r) = self._match_one(self.REGION_MODE_STMT, line) + (mode, r) = _match_one(self.REGION_MODE_STMT, line) if mode: yield RegionModeStmt.from_gerber(line) line = r @@ -226,7 +227,7 @@ class GerberParser(object): continue # Quadrant Mode - (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) + (mode, r) = _match_one(self.QUAD_MODE_STMT, line) if mode: yield QuadrantModeStmt.from_gerber(line) line = r @@ -234,7 +235,7 @@ class GerberParser(object): continue # coord - (coord, r) = self._match_one(self.COORD_STMT, line) + (coord, r) = _match_one(self.COORD_STMT, line) if coord: yield CoordStmt.from_dict(coord, self.settings) line = r @@ -242,7 +243,7 @@ class GerberParser(object): continue # aperture selection - (aperture, r) = self._match_one(self.APERTURE_STMT, line) + (aperture, r) = _match_one(self.APERTURE_STMT, line) if aperture: yield ApertureStmt(**aperture) @@ -251,7 +252,7 @@ class GerberParser(object): continue # comment - (comment, r) = self._match_one(self.COMMENT_STMT, line) + (comment, r) = _match_one(self.COMMENT_STMT, line) if comment: yield CommentStmt(comment["comment"]) did_something = True @@ -259,7 +260,7 @@ class GerberParser(object): continue # parameter - (param, r) = self._match_one_from_many(self.PARAM_STMT, line) + (param, r) = _match_one_from_many(self.PARAM_STMT, line) if param: if param["param"] == "FS": stmt = FSParamStmt.from_dict(param) @@ -292,7 +293,7 @@ class GerberParser(object): continue # eof - (eof, r) = self._match_one(self.EOF_STMT, line) + (eof, r) = _match_one(self.EOF_STMT, line) if eof: yield EofStmt() did_something = True @@ -311,17 +312,125 @@ class GerberParser(object): 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 evaluate(self, stmt): + """ Evaluate Gerber statement and update image accordingly. - 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):]) + This method is called once for each statement in the file as it + is parsed. + Parameters + ---------- + statement : Statement + Gerber/Excellon statement to evaluate. + + """ + 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) + + elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): + self._evaluate_mode(stmt) + + else: + raise Exception("Invalid statement to evaluate") + + + def _define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + diameter = float(modifiers[0][0]) + aperture = Circle(position=None, diameter=diameter) + elif shape == 'R': + width = float(modifiers[0][0]) + height = float(modifiers[0][1]) + aperture = Rectangle(position=None, width=width, height=height) + elif shape == 'O': + width = float(modifiers[0][0]) + height = float(modifiers[0][1]) + aperture = Obround(position=None, width=width, height=height) + self.apertures[d] = aperture + + def _evaluate_mode(self, stmt): + if stmt.type == 'RegionMode': + if self.region_mode == 'on' and stmt.mode == 'off': + self.primitives.append(Region(self.current_region, self.level_polarity)) + self.current_region = None + self.region_mode = stmt.mode + elif stmt.type == 'QuadrantMode': + self.quadrant_mode = stmt.mode + + def _evaluate_param(self, stmt): + if stmt.param == "FS": + self.settings.zero_suppression = stmt.zero_suppression + self.settings.format = stmt.format + self.settings.notation = stmt.notation + elif stmt.param == "MO": + self.settings.units = stmt.mode + elif stmt.param == "IP": + self.image_polarity = stmt.ip + elif stmt.param == "LP": + self.level_polarity = stmt.lp + elif stmt.param == "AD": + self._define_aperture(stmt.d, stmt.shape, stmt.modifiers) + + def _evaluate_coord(self, stmt): + x = self.x if stmt.x is None else stmt.x + y = self.y if stmt.y is None else stmt.y + if stmt.function in ("G01", "G1"): + self.interpolation = 'linear' + elif stmt.function in ('G02', 'G2', 'G03', 'G3'): + self.interpolation = 'arc' + self.direction = ('clockwise' if stmt.function in ('G02', 'G2') + else 'counterclockwise') + if stmt.op == "D01": + if self.region_mode == 'on': + if self.current_region is None: + self.current_region = [(self.x, self.y), ] + self.current_region.append((x, y,)) + else: + start = (self.x, self.y) + end = (x, y) + width = self.apertures[self.aperture].stroke_width + if self.interpolation == 'linear': + self.primitives.append(Line(start, end, width, self.level_polarity)) + else: + center = (start[0] + stmt.i, start[1] + stmt.j) + self.primitives.append(Arc(start, end, center, self.direction, width, self.level_polarity)) + + elif stmt.op == "D02": + pass + + elif stmt.op == "D03": + primitive = copy.deepcopy(self.apertures[self.aperture]) + primitive.position = (x, y) + primitive.level_polarity = self.level_polarity + self.primitives.append(primitive) + self.x, self.y = x, y + + def _evaluate_aperture(self, stmt): + self.aperture = stmt.d + + +def _match_one(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(exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) -- cgit From 18e3b87625ddb739faeddffcaed48e12db6c7e8b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 19 Oct 2014 22:23:00 -0400 Subject: Test update --- gerber/rs274x.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 39693c9..739c253 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -112,8 +112,6 @@ class GerberFile(CamFile): f.write(statement.to_gerber()) - - class GerberParser(object): """ GerberParser """ @@ -324,21 +322,21 @@ class GerberParser(object): Gerber/Excellon statement to evaluate. """ - if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): - return + if isinstance(stmt, CoordStmt): + self._evaluate_coord(stmt) elif isinstance(stmt, ParamStmt): self._evaluate_param(stmt) - elif isinstance(stmt, CoordStmt): - self._evaluate_coord(stmt) - elif isinstance(stmt, ApertureStmt): self._evaluate_aperture(stmt) elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): self._evaluate_mode(stmt) + elif isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + return + else: raise Exception("Invalid statement to evaluate") -- cgit From 4f076d7b769b0f488888d268a9a199b7545afdd7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 26 Oct 2014 17:59:57 -0400 Subject: Merge aperture fixses from upstream --- gerber/rs274x.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 739c253..f18a35d 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -118,7 +118,7 @@ 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]+" + NAME = r"[a-zA-Z_$][a-zA-Z_$0-9]+" FUNCTION = r"G\d{2}" COORD_OP = r"D[0]?[123]" @@ -128,10 +128,10 @@ class GerberParser(object): 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_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) + AD_MACRO = r"(?PAD)D(?P\d+)(?P{name})[,]?(?P[^,]+)?".format(name=NAME) AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) # begin deprecated @@ -140,7 +140,7 @@ class GerberParser(object): 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) + PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, OF, IN, LN) PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] COORD_STMT = re.compile(( -- cgit From 777ffd385b4b380df0ffebf33b1dee0b99bde53d Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 26 Oct 2014 21:39:51 -0200 Subject: Fix parsing of Unknown commands --- gerber/rs274x.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index f18a35d..627fcf9 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -308,6 +308,10 @@ class GerberParser(object): if line.find('*') > 0: yield UnknownStmt(line) + did_something = True + line = "" + continue + oldline = line def evaluate(self, stmt): -- cgit From 8f584d6396e82ad9929fe771d72b21b5303b9cd6 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 26 Oct 2014 22:26:04 -0200 Subject: Fix parsing for AM macros and support for zero sized circle primitives --- gerber/rs274x.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 627fcf9..08c492a 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -127,11 +127,11 @@ class GerberParser(object): 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_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) + AD_MACRO = r"(?PAD)D(?P\d+)(?P{name})[,]?(?P[^,]*)?".format(name=NAME) AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) # begin deprecated -- cgit From 3ffa9238c401cf0a872e1abae872a356d5f15f95 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 26 Oct 2014 22:26:50 -0200 Subject: Add simple hack to allow evaluation when gerber have macros and polygon --- gerber/rs274x.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 08c492a..c01f027 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -412,9 +412,11 @@ class GerberParser(object): elif stmt.op == "D03": primitive = copy.deepcopy(self.apertures[self.aperture]) - primitive.position = (x, y) - primitive.level_polarity = self.level_polarity - self.primitives.append(primitive) + # XXX: temporary fix because there are no primitives for Macros and Polygon + if primitive is not None: + primitive.position = (x, y) + primitive.level_polarity = self.level_polarity + self.primitives.append(primitive) self.x, self.y = x, y def _evaluate_aperture(self, stmt): -- cgit From 171876bca01d5c92afd3fa29f2c62c5d50f65511 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 26 Oct 2014 22:27:12 -0200 Subject: style change --- gerber/rs274x.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index c01f027..f7be44d 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -386,12 +386,13 @@ class GerberParser(object): def _evaluate_coord(self, stmt): x = self.x if stmt.x is None else stmt.x y = self.y if stmt.y is None else stmt.y + if stmt.function in ("G01", "G1"): self.interpolation = 'linear' elif stmt.function in ('G02', 'G2', 'G03', 'G3'): self.interpolation = 'arc' - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') - else 'counterclockwise') + self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') + if stmt.op == "D01": if self.region_mode == 'on': if self.current_region is None: -- cgit From ab69ee0172353e64fbe5099a974341e88feaf24b Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Mon, 10 Nov 2014 12:24:09 -0200 Subject: Bunch of small fixes to improve Gerber read/write. --- gerber/rs274x.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index f7be44d..a41760e 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -109,7 +109,7 @@ class GerberFile(CamFile): """ with open(filename, 'w') as f: for statement in self.statements: - f.write(statement.to_gerber()) + f.write(statement.to_gerber() + "\n") class GerberParser(object): @@ -149,7 +149,7 @@ class GerberParser(object): 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+)\*") + APERTURE_STMT = re.compile(r"(?PG54)?D(?P\d+)\*") COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") -- cgit From 29deffcf77e963ae81aec9f8cbc61b029f3052d5 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 5 Dec 2014 23:59:28 -0500 Subject: add ipc2581 primitives --- gerber/rs274x.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index a41760e..6dbcc63 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -403,10 +403,10 @@ class GerberParser(object): end = (x, y) width = self.apertures[self.aperture].stroke_width if self.interpolation == 'linear': - self.primitives.append(Line(start, end, width, self.level_polarity)) + self.primitives.append(Line(start, end, width, level_polarity=self.level_polarity)) else: center = (start[0] + stmt.i, start[1] + stmt.j) - self.primitives.append(Arc(start, end, center, self.direction, width, self.level_polarity)) + self.primitives.append(Arc(start, end, center, self.direction, width, level_polarity=self.level_polarity)) elif stmt.op == "D02": pass -- cgit From be5b94b8c09f647e5e19f795927060f75461c283 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Mon, 15 Dec 2014 23:38:27 -0200 Subject: Fix parsing for OrCAD. * Modify the way we parse parameters to allow more than one parameter in a single line as in the following example: %FSLAX55Y55*MOIN*% %IR0*IPPOS*OFA0.00000B0.00000*MIA0B0*SFA1.00000B1.00000*% (this is from OrCAD 16 default output) * Add missing deprecated parameters. * Change API to use given FileSettings on output. This allows us to use pcb-tools to convert between FS formats. --- gerber/rs274x.py | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 6dbcc63..8f4a171 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -104,12 +104,13 @@ class GerberFile(CamFile): return (xbounds, ybounds) - def write(self, filename): + def write(self, filename, settings=None): """ Write data out to a gerber file """ with open(filename, 'w') as f: for statement in self.statements: - f.write(statement.to_gerber() + "\n") + f.write(statement.to_gerber(settings or self.settings)) + f.write("\n") class GerberParser(object): @@ -125,7 +126,6 @@ class GerberParser(object): 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[^,]*)" @@ -135,13 +135,18 @@ class GerberParser(object): AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) # begin deprecated - OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) + AS = r"(?PAS)(?P(AXBY)|(AYBX))" IN = r"(?PIN)(?P.*)" + IP = r"(?PIP)(?P(POS|NEG))" + IR = r"(?PIR)(?P{number})".format(number=NUMBER) + MI = r"(?PMI)(A(?P0|1))?(B(?P0|1))?" + OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) + SF = r"(?PSF)(?P.*)" LN = r"(?PLN)(?P.*)" # end deprecated - PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, OF, IN, LN) - PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] + PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) + PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] COORD_STMT = re.compile(( r"(?P{function})?" @@ -149,7 +154,7 @@ class GerberParser(object): r"(I(?P{number}))?(J(?P{number}))?" r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) - APERTURE_STMT = re.compile(r"(?PG54)?D(?P\d+)\*") + APERTURE_STMT = re.compile(r"(?P(G54)|G55)?D(?P\d+)\*") COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") @@ -270,8 +275,6 @@ class GerberParser(object): 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": @@ -284,8 +287,26 @@ class GerberParser(object): yield INParamStmt.from_dict(param) elif param["param"] == "LN": yield LNParamStmt.from_dict(param) + # deprecated commands AS, IN, IP, IR, MI, OF, SF, LN + elif param["param"] == "AS": + yield ASParamStmt.from_dict(param) + elif param["param"] == "IN": + yield INParamStmt.from_dict(param) + elif param["param"] == "IP": + yield IPParamStmt.from_dict(param) + elif param["param"] == "IR": + yield IRParamStmt.from_dict(param) + elif param["param"] == "MI": + yield MIParamStmt.from_dict(param) + elif param["param"] == "OF": + yield OFParamStmt.from_dict(param) + elif param["param"] == "SF": + yield SFParamStmt.from_dict(param) + elif param["param"] == "LN": + yield LNParamStmt.from_dict(param) else: yield UnknownStmt(line) + did_something = True line = r continue @@ -298,14 +319,6 @@ class GerberParser(object): 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) did_something = True -- cgit From 53ee7566097b5a26cd3a1dab1d730f9606d767e6 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Tue, 13 Jan 2015 23:12:27 -0200 Subject: Fix region primitive creation --- gerber/rs274x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 8f4a171..3ec7429 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -376,7 +376,7 @@ class GerberParser(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self.primitives.append(Region(self.current_region, self.level_polarity)) + self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity)) self.current_region = None self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': -- cgit From cbb662491c273e97cfceed94b29a77ce865244dd Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Wed, 14 Jan 2015 03:15:52 -0200 Subject: Refactor AM aperture handling and add unit conversion support * Add support to convert between metric/impertial * AM primitives are now properly created and can be converted between metric/imperial. (only Outline primitive is supported, no rendering yet) --- gerber/rs274x.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 3ec7429..2e5a3ec 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -361,15 +361,15 @@ class GerberParser(object): def _define_aperture(self, d, shape, modifiers): aperture = None if shape == 'C': - diameter = float(modifiers[0][0]) + diameter = modifiers[0][0] aperture = Circle(position=None, diameter=diameter) elif shape == 'R': - width = float(modifiers[0][0]) - height = float(modifiers[0][1]) + width = modifiers[0][0] + height = modifiers[0][1] aperture = Rectangle(position=None, width=width, height=height) elif shape == 'O': - width = float(modifiers[0][0]) - height = float(modifiers[0][1]) + width = modifiers[0][0] + height = modifiers[0][1] aperture = Obround(position=None, width=width, height=height) self.apertures[d] = aperture -- cgit From 208149d6769e608918e04977a5af110bce9c5bd6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 26 Jan 2015 22:24:45 -0500 Subject: merge upstream changes --- gerber/rs274x.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 2e5a3ec..abd7366 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -113,6 +113,7 @@ class GerberFile(CamFile): f.write("\n") + class GerberParser(object): """ GerberParser """ @@ -156,6 +157,7 @@ class GerberParser(object): APERTURE_STMT = re.compile(r"(?P(G54)|G55)?D(?P\d+)\*") + COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") EOF_STMT = re.compile(r"(?PM02)\*") -- cgit From 8f69c1dfa281b6486c8fce16c1d58acef70c7ae7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 12 Feb 2015 11:28:50 -0500 Subject: Update line primitive to take aperture parameter This fixes the exception referenced in #12. Still need to add rendering code for rectangle aperture lines and arcs. Rectangle strokes will be drawn as polygons by the rendering backends. --- gerber/rs274x.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index abd7366..71ca111 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -416,12 +416,12 @@ class GerberParser(object): else: start = (self.x, self.y) end = (x, y) - width = self.apertures[self.aperture].stroke_width + #width = self.apertures[self.aperture].stroke_width if self.interpolation == 'linear': - self.primitives.append(Line(start, end, width, level_polarity=self.level_polarity)) + self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity)) else: center = (start[0] + stmt.i, start[1] + stmt.j) - self.primitives.append(Arc(start, end, center, self.direction, width, level_polarity=self.level_polarity)) + self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity)) elif stmt.op == "D02": pass -- cgit From 288ac27084b47166ac662402ea340d0aa25d8f56 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 04:31:23 -0500 Subject: Get unit conversion working for Gerber/Excellon files Started operations module for file operations/transforms --- gerber/rs274x.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 71ca111..21947e1 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -18,14 +18,15 @@ """ This module provides an RS-274-X class and parser. """ - import copy import json import re + from .gerber_statements import * from .primitives import * from .cam import CamFile, FileSettings + def read(filename): """ Read data from filename and return a GerberFile @@ -112,6 +113,21 @@ class GerberFile(CamFile): f.write(statement.to_gerber(settings or self.settings)) f.write("\n") + def to_inch(self): + if self.units != 'inch': + self.units = 'inch' + for statement in self.statements: + statement.to_inch() + for primitive in self.primitives: + primitive.to_inch() + + def to_metric(self): + if self.units != 'metric': + self.units = 'metric' + for statement in self.statements: + statement.to_metric() + for primitive in self.primitives: + primitive.to_metric() class GerberParser(object): -- cgit From 5966d7830bda7f37ed5ddcc1bfccb93e7f780eaa Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 23:13:23 -0500 Subject: Add offset operation --- gerber/rs274x.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 21947e1..2dee7cf 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -129,6 +129,12 @@ class GerberFile(CamFile): for primitive in self.primitives: primitive.to_metric() + def offset(self, x_offset=0, y_offset=0): + for statement in self.statements: + statement.offset(x_offset, y_offset) + for primitive in self.primitives: + primitive.offset(x_offset, y_offset) + class GerberParser(object): """ GerberParser -- cgit From 5d764a68908bf905741f91e01c1250b5b64edc52 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Fri, 20 Feb 2015 13:57:19 -0200 Subject: Fix GerberFile.bounds when board origin is negative --- gerber/rs274x.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 2dee7cf..c5c89fb 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -88,22 +88,19 @@ class GerberFile(CamFile): @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)]: + min_x = min_y = 1000000 + max_x = max_y = -1000000 + + for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]: if stmt.x is not None: - if stmt.x < xbounds[0]: - xbounds[0] = stmt.x - elif stmt.x > xbounds[1]: - xbounds[1] = stmt.x + min_x = min(stmt.x, min_x) + max_x = max(stmt.x, max_x) + if stmt.y is not None: - if stmt.y < ybounds[0]: - ybounds[0] = stmt.y - elif stmt.y > ybounds[1]: - ybounds[1] = stmt.y - return (xbounds, ybounds) + min_y = min(stmt.y, min_y) + max_y = max(stmt.y, max_y) + return ((min_x, max_x), (min_y, max_y)) def write(self, filename, settings=None): """ Write data out to a gerber file -- cgit From 670d3fbbd7ebfb69bd223ac30b73ec47b195b380 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Tue, 3 Mar 2015 03:41:55 -0300 Subject: Add aperture macro parsing and evaluation. Aperture macros can get complex with arithmetical operations, variables and variables substitution. Current pcb-tools code just read each macro block as an independent unit, this cannot deal with variables that get changed after used. This patch splits the task in two: first we parse all macro content and creates a bytecode representation of all operations. This bytecode representation will be executed when an AD command is issues passing the required parameters. Parsing is heavily based on gerbv using a Shunting Yard approach to math parsing. Integration with rs274x.py code is not finished as I need to figure out how to integrate the final macro primitives with the graphical primitives already in use. --- gerber/rs274x.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index c5c89fb..69485a8 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -189,6 +189,7 @@ class GerberParser(object): self.statements = [] self.primitives = [] self.apertures = {} + self.macros = {} self.current_region = None self.x = 0 self.y = 0 @@ -392,6 +393,12 @@ class GerberParser(object): width = modifiers[0][0] height = modifiers[0][1] aperture = Obround(position=None, width=width, height=height) + elif shape == 'P': + # FIXME: not supported yet? + pass + else: + aperture = self.macros[shape].evaluate(modifiers) + self.apertures[d] = aperture def _evaluate_mode(self, stmt): @@ -414,6 +421,8 @@ class GerberParser(object): self.image_polarity = stmt.ip elif stmt.param == "LP": self.level_polarity = stmt.lp + elif stmt.param == "AM": + self.macros[stmt.name] = stmt elif stmt.param == "AD": self._define_aperture(stmt.d, stmt.shape, stmt.modifiers) @@ -449,9 +458,14 @@ class GerberParser(object): primitive = copy.deepcopy(self.apertures[self.aperture]) # XXX: temporary fix because there are no primitives for Macros and Polygon if primitive is not None: - primitive.position = (x, y) - primitive.level_polarity = self.level_polarity - self.primitives.append(primitive) + # XXX: just to make it easy to spot + if isinstance(primitive, type([])): + print primitive[0].to_gerber() + else: + primitive.position = (x, y) + primitive.level_polarity = self.level_polarity + self.primitives.append(primitive) + self.x, self.y = x, y def _evaluate_aperture(self, stmt): -- cgit From a13b981c1c2ea9ede39e9821d9ba818566f044de Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 5 Mar 2015 14:43:30 -0300 Subject: Fix tests for macros with no variables. All AM*Primitive classes now handles float for all but the code modifiers. This simplifies the reading/parsing. --- gerber/rs274x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 69485a8..d2844c9 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -397,7 +397,7 @@ class GerberParser(object): # FIXME: not supported yet? pass else: - aperture = self.macros[shape].evaluate(modifiers) + aperture = self.macros[shape].build(modifiers) self.apertures[d] = aperture -- cgit From adc1ff6d72ac1201517fffcd8e377768be022e91 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 5 Mar 2015 14:58:36 -0300 Subject: Fix for py3 --- gerber/rs274x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index d2844c9..a3a27e9 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -460,7 +460,7 @@ class GerberParser(object): if primitive is not None: # XXX: just to make it easy to spot if isinstance(primitive, type([])): - print primitive[0].to_gerber() + print(primitive[0].to_gerber()) else: primitive.position = (x, y) primitive.level_polarity = self.level_polarity -- cgit From b93804ed9a3400099afceacfe5a809ae8bded2a4 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Tue, 7 Apr 2015 18:22:02 -0300 Subject: Add unspecified FS D leading zeros format FS D leading zero format (probably form Direct) is an unspecified coordinate format where all numbers are specified with both leading and trailing zeros. --- gerber/rs274x.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index a3a27e9..07fce17 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -139,12 +139,9 @@ class GerberParser(object): NUMBER = r"[\+-]?\d+" DECIMAL = r"[\+-]?\d+([.]?\d+)?" STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" - NAME = r"[a-zA-Z_$][a-zA-Z_$0-9]+" - FUNCTION = r"G\d{2}" + NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+" - 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])" + FS = r"(?PFS)(?P(L|T|D))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" MO = r"(?PMO)(?P(MM|IN))" LP = r"(?PLP)(?P(D|C))" AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,]?(?P[^,]*)?" -- cgit From d1c74317c8df83da5d9f01b74c28be2b467fa300 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Tue, 7 Apr 2015 18:26:33 -0300 Subject: Add some deprecated but still found statements --- gerber/rs274x.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 07fce17..fbedc77 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -160,23 +160,28 @@ class GerberParser(object): OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) SF = r"(?PSF)(?P.*)" LN = r"(?PLN)(?P.*)" + DEPRECATED_UNIT = re.compile(r'(?PG7[01])\*') + DEPRECATED_FORMAT = re.compile(r'(?PG9[01])\*') # end deprecated PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) + PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] + COORD_FUNCTION = r"G0?[123]" + COORD_OP = r"D0?[123]" + 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"(?P(G54)|G55)?D(?P\d+)\*") + r"(?P{op})?\*".format(number=NUMBER, function=COORD_FUNCTION, op=COORD_OP))) + APERTURE_STMT = re.compile(r"(?P(G54)|(G55))?D(?P\d+)\*") - COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") + COMMENT_STMT = re.compile(r"G0?4(?P[^*]*)(\*)?") - EOF_STMT = re.compile(r"(?PM02)\*") + EOF_STMT = re.compile(r"(?PM[0]?[012])\*") REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') @@ -330,6 +335,21 @@ class GerberParser(object): line = r continue + # deprecated codes (parsed but ignored) + (deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line) + if deprecated_unit: + yield DeprecatedStmt.from_gerber(line) + line = r + did_something = True + continue + + (deprecated_format, r) = _match_one(self.DEPRECATED_FORMAT, line) + if deprecated_format: + yield DeprecatedStmt.from_gerber(line) + line = r + did_something = True + continue + # eof (eof, r) = _match_one(self.EOF_STMT, line) if eof: @@ -370,7 +390,7 @@ class GerberParser(object): elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): self._evaluate_mode(stmt) - elif isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + elif isinstance(stmt, (CommentStmt, UnknownStmt, DeprecatedStmt, EofStmt)): return else: -- cgit From 1ea7e14ba593a6fff7b4f7895875a7e93a03b1c8 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Tue, 7 Apr 2015 18:28:03 -0300 Subject: Fix CoordStmt with missing i/j offsets --- gerber/rs274x.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index fbedc77..9403517 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -461,11 +461,13 @@ class GerberParser(object): else: start = (self.x, self.y) end = (x, y) - #width = self.apertures[self.aperture].stroke_width + if self.interpolation == 'linear': self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity)) else: - center = (start[0] + stmt.i, start[1] + stmt.j) + i = 0 if stmt.i is None else stmt.i + j = 0 if stmt.j is None else stmt.j + center = (start[0] + i, start[1] + j) self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity)) elif stmt.op == "D02": -- cgit From d6bb61eec681d5151fb5869b6a8e4618eba1398d Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Mon, 13 Apr 2015 16:55:03 -0300 Subject: Fix issue where D01 and D03 are implicit. Based on code from @rdprescott. --- gerber/rs274x.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 9403517..6069c03 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -195,7 +195,7 @@ class GerberParser(object): self.current_region = None self.x = 0 self.y = 0 - + self.op = "D02" self.aperture = 0 self.interpolation = 'linear' self.direction = 'clockwise' @@ -453,7 +453,10 @@ class GerberParser(object): self.interpolation = 'arc' self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') - if stmt.op == "D01": + if stmt.op: + self.op = stmt.op + + if self.op == "D01": if self.region_mode == 'on': if self.current_region is None: self.current_region = [(self.x, self.y), ] @@ -470,10 +473,10 @@ class GerberParser(object): center = (start[0] + i, start[1] + j) self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity)) - elif stmt.op == "D02": + elif self.op == "D02": pass - elif stmt.op == "D03": + elif self.op == "D03": primitive = copy.deepcopy(self.apertures[self.aperture]) # XXX: temporary fix because there are no primitives for Macros and Polygon if primitive is not None: -- cgit From a3cce62be741cb2bc1e65165ba4f0b45c8838b60 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 23 Apr 2015 13:38:01 -0300 Subject: Fix Gerber generation for coord blocks with implicit op code --- gerber/rs274x.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 6069c03..606d27f 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -455,6 +455,9 @@ class GerberParser(object): if stmt.op: self.op = stmt.op + else: + # no implicit op allowed, force here if coord block doesn't have it + stmt.op = self.op if self.op == "D01": if self.region_mode == 'on': -- cgit From 390838fc8b70c9b105fdc1d3e35a4533b27faa83 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 24 Apr 2015 10:54:13 -0400 Subject: Fix for #25. Checking was happening at the gerber/excellon file level, but I added units checking at the primitive level so the use case shown in the example is covered. Might want to throw a bunch more assertions in the test code (i started doing a few) to cover multiple calls to unit conversion functions --- gerber/rs274x.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 606d27f..3dcddb4 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -469,12 +469,12 @@ class GerberParser(object): end = (x, y) if self.interpolation == 'linear': - self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity)) + self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) else: i = 0 if stmt.i is None else stmt.i j = 0 if stmt.j is None else stmt.j center = (start[0] + i, start[1] + j) - self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity)) + self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) elif self.op == "D02": pass @@ -489,6 +489,7 @@ class GerberParser(object): else: primitive.position = (x, y) primitive.level_polarity = self.level_polarity + primitive.units = self.settings.units self.primitives.append(primitive) self.x, self.y = x, y -- cgit From 8ec3077be988681bbbafcef18ea3a2f84dd61b2b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 16 May 2015 09:45:34 -0400 Subject: Add checks to ensure statement unit conversions are idempotent --- gerber/rs274x.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 3dcddb4..20baaa7 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -214,6 +214,10 @@ class GerberParser(object): self.evaluate(stmt) self.statements.append(stmt) + # Initialize statement units + for stmt in self.statements: + stmt.units = self.settings.units + return GerberFile(self.statements, self.settings, self.primitives, filename) def dump_json(self): -- cgit From 2fe5f36db233e6c45e0d6b2443b025baf173211e Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 21 May 2015 15:54:32 -0300 Subject: Fix ADD statement parsing for concatened statements. ADDxxx param statements were too greedy on the mofidiers and were matching more than it should in cases where there are no newlines after the statement like: '%ADD12C,0.305*%%LPD*%', in a single line. The '%' was not exluded form modifiers so it got confused with the %LPD*% concatened. top_copper.GTL example was changed to be in a single line now with no spaces at all and it works well. --- gerber/rs274x.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 20baaa7..5d1f5fe 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -144,12 +144,12 @@ class GerberParser(object): FS = r"(?PFS)(?P(L|T|D))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" MO = r"(?PMO)(?P(MM|IN))" 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) + 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 AS = r"(?PAS)(?P(AXBY)|(AYBX))" -- cgit From aff36a4dca0d2d06b00c5f1e1a0703400fbe3b6b Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 21 May 2015 16:15:55 -0300 Subject: Fix multiline read of mixed statements (%XXX*% followed by DNN*) We now check if there is a %XXX*% command inside the line before considering it a multiline statement. --- gerber/rs274x.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 5d1f5fe..2af3ed6 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -241,10 +241,11 @@ class GerberParser(object): continue # deal with multi-line parameters - if line.startswith("%") and not line.endswith("%"): + if line.startswith("%") and not line.endswith("%") and not "%" in line[1:]: oldline = line continue + did_something = True # make sure we do at least one loop while did_something and len(line) > 0: did_something = False @@ -292,6 +293,7 @@ class GerberParser(object): # parameter (param, r) = _match_one_from_many(self.PARAM_STMT, line) + if param: if param["param"] == "FS": stmt = FSParamStmt.from_dict(param) -- cgit From 9e36d7e21d2906e22c91350ea0fee0d989d58584 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 21 May 2015 17:15:54 -0300 Subject: G70/G71 are now interpreted as MOParamStmt. Got a bunch of metric files with no MOMM but only G71, this should be pretty mush harmless. --- gerber/rs274x.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 2af3ed6..b4963d1 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -341,10 +341,12 @@ class GerberParser(object): line = r continue - # deprecated codes (parsed but ignored) + # deprecated codes (deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line) if deprecated_unit: - yield DeprecatedStmt.from_gerber(line) + stmt = MOParamStmt(param="MO", mo="inch" if "G70" in deprecated_unit["mode"] else "metric") + self.settings.units = stmt.mode + yield stmt line = r did_something = True continue -- cgit From cb2fa34e881a389cf8a4bc98fd12be662ff687f8 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 9 Aug 2015 15:11:13 -0400 Subject: Add support for arcs in regions. This fixes the circular cutout issue described in #32. Regions were previously stored as a collection of points, now they are stored as a collection of line and arc primitives. --- gerber/rs274x.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index b4963d1..1df3646 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -468,22 +468,29 @@ class GerberParser(object): stmt.op = self.op if self.op == "D01": - if self.region_mode == 'on': - if self.current_region is None: - self.current_region = [(self.x, self.y), ] - self.current_region.append((x, y,)) - else: - start = (self.x, self.y) - end = (x, y) + start = (self.x, self.y) + end = (x, y) - if self.interpolation == 'linear': + if self.interpolation == 'linear': + if self.region_mode == 'off': self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) else: - i = 0 if stmt.i is None else stmt.i - j = 0 if stmt.j is None else stmt.j - center = (start[0] + i, start[1] + j) + if self.current_region is None: + self.current_region = [Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units),] + else: + self.current_region.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + else: + i = 0 if stmt.i is None else stmt.i + j = 0 if stmt.j is None else stmt.j + center = (start[0] + i, start[1] + j) + if self.region_mode == 'off': self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) - + else: + if self.current_region is None: + self.current_region = [Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units),] + else: + self.current_region.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + elif self.op == "D02": pass -- cgit From dd63b169f177389602e17bc6ced53bd0f1ba0de3 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 10 Oct 2015 16:51:21 -0400 Subject: Allow files to be read from strings per #37 Adds a loads() method to the top level module which generates a GerberFile or ExcellonFile from a string --- gerber/rs274x.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 1df3646..000f7a1 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -21,6 +21,7 @@ import copy import json import re +from cStringIO import StringIO from .gerber_statements import * from .primitives import * @@ -43,6 +44,9 @@ def read(filename): return GerberParser().parse(filename) +def loads(data): + return GerberParser().parse_raw(data) + class GerberFile(CamFile): """ A class representing a single gerber file @@ -75,7 +79,6 @@ class GerberFile(CamFile): def __init__(self, statements, settings, primitives, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, filename) - @property def comments(self): return [comment.comment for comment in self.statements @@ -205,12 +208,14 @@ class GerberParser(object): self.quadrant_mode = 'multi-quadrant' self.step_and_repeat = (1, 1, 0, 0) - def parse(self, filename): - fp = open(filename, "r") - data = fp.readlines() + with open(filename, "r") as fp: + data = fp.read() + return self.parse_raw(data, filename=None) - for stmt in self._parse(data): + def parse_raw(self, data, filename=None): + lines = [line for line in StringIO(data)] + for stmt in self._parse(lines): self.evaluate(stmt) self.statements.append(stmt) @@ -225,10 +230,10 @@ class GerberParser(object): return json.dumps(stmts) def dump_str(self): - s = "" + string = "" for stmt in self.statements: - s += str(stmt) + "\n" - return s + string += str(stmt) + "\n" + return string def _parse(self, data): oldline = '' @@ -404,7 +409,6 @@ class GerberParser(object): else: raise Exception("Invalid statement to evaluate") - def _define_aperture(self, d, shape, modifiers): aperture = None if shape == 'C': @@ -490,7 +494,7 @@ class GerberParser(object): self.current_region = [Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units),] else: self.current_region.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) - + elif self.op == "D02": pass -- cgit From 10d9028e1fdf7431baee73c7f1474d2134bac5fa Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 10 Oct 2015 17:02:45 -0400 Subject: Python 3 fix --- gerber/rs274x.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 000f7a1..210b590 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -21,8 +21,12 @@ import copy import json import re -from cStringIO import StringIO +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + from .gerber_statements import * from .primitives import * from .cam import CamFile, FileSettings -- cgit From 2208fe22052bf04816e5492cdcb4469c19644e4b Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Fri, 13 Nov 2015 04:17:27 -0200 Subject: Fix issue when a region is created as the first graphical object in a file When regions were the first thing draw there is no current aperture defined, as regions do not require an aperture, so we use an zeroed Circle as aperture in this case. Gerber spec says that apertures have no graphical meaning for regions, so this should be enough. --- gerber/rs274x.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 210b590..72d7e95 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -483,10 +483,13 @@ class GerberParser(object): if self.region_mode == 'off': self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) else: + # from gerber spec revision J3, Section 4.5, page 55: + # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. + # The current aperture is associated with the region. This has no graphical effect, but allows all its attributes to be applied to the region. if self.current_region is None: - self.current_region = [Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units),] + self.current_region = [Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),] else: - self.current_region.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region.append(Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units)) else: i = 0 if stmt.i is None else stmt.i j = 0 if stmt.j is None else stmt.j -- cgit From 6e29b9bcae8167dbb9c75e5a79e09886b952e988 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 15 Nov 2015 22:28:56 -0200 Subject: Use Python's universal newlines to open files --- gerber/rs274x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 72d7e95..9fd63da 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -213,7 +213,7 @@ class GerberParser(object): self.step_and_repeat = (1, 1, 0, 0) def parse(self, filename): - with open(filename, "r") as fp: + with open(filename, "rU") as fp: data = fp.read() return self.parse_raw(data, filename=None) -- cgit From f2b075e338fcd103a7af6e20e27f3960e63d20e4 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 18 Nov 2015 11:26:20 +0800 Subject: Regions with arcs would crash if they occured before any command to set the aperture --- gerber/rs274x.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 9fd63da..96bd136 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -498,9 +498,9 @@ class GerberParser(object): self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) else: if self.current_region is None: - self.current_region = [Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units),] + self.current_region = [Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),] else: - self.current_region.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region.append(Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units)) elif self.op == "D02": pass -- cgit From 221f67d8fe77ecae6c8e99db767eace5da0c1f9e Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Thu, 3 Dec 2015 09:42:45 +0800 Subject: Move the coordinate matching to the beginning since most of the items are coordinates. For large files, this decreases total time by 10-20% --- gerber/rs274x.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 96bd136..3e262b3 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -258,6 +258,14 @@ class GerberParser(object): did_something = True # make sure we do at least one loop while did_something and len(line) > 0: did_something = False + + # coord + (coord, r) = _match_one(self.COORD_STMT, line) + if coord: + yield CoordStmt.from_dict(coord, self.settings) + line = r + did_something = True + continue # Region Mode (mode, r) = _match_one(self.REGION_MODE_STMT, line) @@ -275,19 +283,10 @@ class GerberParser(object): did_something = True continue - # coord - (coord, r) = _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) = _match_one(self.APERTURE_STMT, line) if aperture: yield ApertureStmt(**aperture) - did_something = True line = r continue -- cgit From 1cb269131bc52f0b1a1e69cef0466f2d994d52a8 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 19 Dec 2015 21:54:29 -0500 Subject: Allow negative render of soldermask per #50 Update example code and rendering to show change --- gerber/rs274x.py | 66 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 27 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 9fd63da..d9fd317 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -26,7 +26,7 @@ try: from cStringIO import StringIO except(ImportError): from io import StringIO - + from .gerber_statements import * from .primitives import * from .cam import CamFile, FileSettings @@ -49,8 +49,21 @@ def read(filename): def loads(data): + """ Generate a GerberFile object from rs274x data in memory + + Parameters + ---------- + data : string + string containing gerber file contents + + Returns + ------- + file : :class:`gerber.rs274x.GerberFile` + A GerberFile created from the specified file. + """ return GerberParser().parse_raw(data) + class GerberFile(CamFile): """ A class representing a single gerber file @@ -215,7 +228,7 @@ class GerberParser(object): def parse(self, filename): with open(filename, "rU") as fp: data = fp.read() - return self.parse_raw(data, filename=None) + return self.parse_raw(data, filename) def parse_raw(self, data, filename=None): lines = [line for line in StringIO(data)] @@ -254,27 +267,10 @@ class GerberParser(object): oldline = line continue - did_something = True # make sure we do at least one loop while did_something and len(line) > 0: did_something = False - # Region Mode - (mode, r) = _match_one(self.REGION_MODE_STMT, line) - if mode: - yield RegionModeStmt.from_gerber(line) - line = r - did_something = True - continue - - # Quadrant Mode - (mode, r) = _match_one(self.QUAD_MODE_STMT, line) - if mode: - yield QuadrantModeStmt.from_gerber(line) - line = r - did_something = True - continue - # coord (coord, r) = _match_one(self.COORD_STMT, line) if coord: @@ -292,14 +288,6 @@ class GerberParser(object): line = r continue - # comment - (comment, r) = _match_one(self.COMMENT_STMT, line) - if comment: - yield CommentStmt(comment["comment"]) - did_something = True - line = r - continue - # parameter (param, r) = _match_one_from_many(self.PARAM_STMT, line) @@ -350,6 +338,30 @@ class GerberParser(object): line = r continue + # Region Mode + (mode, r) = _match_one(self.REGION_MODE_STMT, line) + if mode: + yield RegionModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # Quadrant Mode + (mode, r) = _match_one(self.QUAD_MODE_STMT, line) + if mode: + yield QuadrantModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # comment + (comment, r) = _match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) + did_something = True + line = r + continue + # deprecated codes (deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line) if deprecated_unit: -- cgit From af5541ac93b222c05229ee05c9def8dbae5f6e25 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 20 Dec 2015 23:54:20 -0500 Subject: Allow renderer to write to memory per #38 Some updates to rendering colors/themes --- gerber/rs274x.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index d9fd317..319d58f 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -110,16 +110,13 @@ class GerberFile(CamFile): def bounds(self): min_x = min_y = 1000000 max_x = max_y = -1000000 - for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]: if stmt.x is not None: min_x = min(stmt.x, min_x) max_x = max(stmt.x, max_x) - if stmt.y is not None: min_y = min(stmt.y, min_y) max_y = max(stmt.y, max_y) - return ((min_x, max_x), (min_y, max_y)) def write(self, filename, settings=None): -- cgit From ff1ad704d5bb7814fdaebc156b727ec3c5f2d1a8 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 30 Dec 2015 18:10:43 +0800 Subject: Work with Diptrace that calls things D3 not D03 --- gerber/rs274x.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 3e262b3..2ecc57d 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -474,7 +474,7 @@ class GerberParser(object): # no implicit op allowed, force here if coord block doesn't have it stmt.op = self.op - if self.op == "D01": + if self.op == "D01" or self.op == "D1": start = (self.x, self.y) end = (x, y) @@ -501,10 +501,10 @@ class GerberParser(object): else: self.current_region.append(Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units)) - elif self.op == "D02": + elif self.op == "D02" or self.op == "D2": pass - elif self.op == "D03": + elif self.op == "D03" or self.op == "D3": primitive = copy.deepcopy(self.apertures[self.aperture]) # XXX: temporary fix because there are no primitives for Macros and Polygon if primitive is not None: -- cgit From 83ae0670d11b5f5ef8ba3a6c362b7129a9e31ab3 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Fri, 8 Jan 2016 00:19:47 +0800 Subject: More stability fixes for poorly constructed files --- gerber/rs274x.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 2ecc57d..12400a1 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -80,8 +80,10 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ - def __init__(self, statements, settings, primitives, filename=None): + def __init__(self, statements, settings, primitives, apertures, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, filename) + + self.apertures = apertures @property def comments(self): @@ -227,7 +229,7 @@ class GerberParser(object): for stmt in self.statements: stmt.units = self.settings.units - return GerberFile(self.statements, self.settings, self.primitives, filename) + return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename) def dump_json(self): stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} -- cgit From 5476da8aa3f4ee424f56f4f2491e7af1c4b7b758 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Fix a bunch of rendering bugs. - 'clear' polarity primitives no longer erase background - Added aperture macro support for polygons - Added aperture macro rendring support - Renderer now creates a new surface for each layer and merges them instead of working directly on a single surface - Updated examples accordingly --- gerber/rs274x.py | 74 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 20 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 319d58f..b19913b 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -93,6 +93,7 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ + def __init__(self, statements, settings, primitives, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, filename) @@ -181,7 +182,8 @@ class GerberParser(object): DEPRECATED_FORMAT = re.compile(r'(?PG9[01])\*') # end deprecated - PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) + PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, + AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] @@ -362,7 +364,8 @@ class GerberParser(object): # deprecated codes (deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line) if deprecated_unit: - stmt = MOParamStmt(param="MO", mo="inch" if "G70" in deprecated_unit["mode"] else "metric") + stmt = MOParamStmt(param="MO", mo="inch" if "G70" in + deprecated_unit["mode"] else "metric") self.settings.units = stmt.mode yield stmt line = r @@ -436,8 +439,9 @@ class GerberParser(object): height = modifiers[0][1] aperture = Obround(position=None, width=width, height=height) elif shape == 'P': - # FIXME: not supported yet? - pass + diameter = modifiers[0][0] + sides = modifiers[0][1] + aperture = Polygon(position=None, radius=diameter/2.0, sides=sides) else: aperture = self.macros[shape].build(modifiers) @@ -446,7 +450,8 @@ class GerberParser(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity)) + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity)) self.current_region = None self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': @@ -476,7 +481,8 @@ class GerberParser(object): self.interpolation = 'linear' elif stmt.function in ('G02', 'G2', 'G03', 'G3'): self.interpolation = 'arc' - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') + self.direction = ('clockwise' if stmt.function in + ('G02', 'G2') else 'counterclockwise') if stmt.op: self.op = stmt.op @@ -490,43 +496,71 @@ class GerberParser(object): if self.interpolation == 'linear': if self.region_mode == 'off': - self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Line(start, end, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) else: # from gerber spec revision J3, Section 4.5, page 55: # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. - # The current aperture is associated with the region. This has no graphical effect, but allows all its attributes to be applied to the region. + # The current aperture is associated with the region. This + # has no graphical effect, but allows all its attributes to + # be applied to the region. if self.current_region is None: - self.current_region = [Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),] - else: - self.current_region.append(Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = [Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units), ] + else: + self.current_region.append(Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units)) else: i = 0 if stmt.i is None else stmt.i j = 0 if stmt.j is None else stmt.j center = (start[0] + i, start[1] + j) if self.region_mode == 'off': - self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Arc(start, end, center, self.direction, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) else: if self.current_region is None: - self.current_region = [Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units),] + self.current_region = [Arc(start, end, center, self.direction, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units), ] else: - self.current_region.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region.append(Arc(start, end, center, self.direction, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) elif self.op == "D02": pass elif self.op == "D03": primitive = copy.deepcopy(self.apertures[self.aperture]) - # XXX: temporary fix because there are no primitives for Macros and Polygon + + if primitive is not None: - # XXX: just to make it easy to spot - if isinstance(primitive, type([])): - print(primitive[0].to_gerber()) - else: + + if not isinstance(primitive, AMParamStmt): primitive.position = (x, y) primitive.level_polarity = self.level_polarity primitive.units = self.settings.units self.primitives.append(primitive) - + else: + # Aperture Macro + for am_prim in primitive.primitives: + renderable = am_prim.to_primitive((x, y), + self.level_polarity, + self.settings.units) + if renderable is not None: + self.primitives.append(renderable) self.x, self.y = x, y def _evaluate_aperture(self, stmt): -- cgit From 5df38c014fd09792995b2b12b1982c535c962c9a Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 28 Jan 2016 12:19:03 -0500 Subject: Cleanup, rendering fixes. fixed rendering of tented vias fixed rendering of semi-transparent layers fixed file type detection issues added some examples --- gerber/rs274x.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index b19913b..76e5101 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -48,7 +48,7 @@ def read(filename): return GerberParser().parse(filename) -def loads(data): +def loads(data, filename=None): """ Generate a GerberFile object from rs274x data in memory Parameters @@ -56,12 +56,15 @@ def loads(data): data : string string containing gerber file contents + filename : string, optional + string containing the filename of the data source + Returns ------- file : :class:`gerber.rs274x.GerberFile` A GerberFile created from the specified file. """ - return GerberParser().parse_raw(data) + return GerberParser().parse_raw(data, filename) class GerberFile(CamFile): -- cgit From 96bdd0f59dbda9b755b0eb28eb44cb9a6eae1410 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 31 Jan 2016 15:24:57 +0800 Subject: Keep track of quadrant mode so we can draw full circles --- gerber/rs274x.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 12400a1..bac5114 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -496,12 +496,12 @@ class GerberParser(object): j = 0 if stmt.j is None else stmt.j center = (start[0] + i, start[1] + j) if self.region_mode == 'off': - self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) else: if self.current_region is None: - self.current_region = [Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),] + self.current_region = [Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units),] else: - self.current_region.append(Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region.append(Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) elif self.op == "D02" or self.op == "D2": pass -- cgit From a765f8aa2c980cdbd6666f32f5be62c88118c152 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 14 Feb 2016 22:06:32 +0800 Subject: Fix convertion of units for apertures and regions --- gerber/rs274x.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index bac5114..185cbc3 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -418,15 +418,15 @@ class GerberParser(object): aperture = None if shape == 'C': diameter = modifiers[0][0] - aperture = Circle(position=None, diameter=diameter) + aperture = Circle(position=None, diameter=diameter, units=self.settings.units) elif shape == 'R': width = modifiers[0][0] height = modifiers[0][1] - aperture = Rectangle(position=None, width=width, height=height) + aperture = Rectangle(position=None, width=width, height=height, units=self.settings.units) elif shape == 'O': width = modifiers[0][0] height = modifiers[0][1] - aperture = Obround(position=None, width=width, height=height) + aperture = Obround(position=None, width=width, height=height, units=self.settings.units) elif shape == 'P': # FIXME: not supported yet? pass @@ -438,7 +438,7 @@ class GerberParser(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity)) + self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) self.current_region = None self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': -- cgit From 991a3687ef741831c860fcbde38651f3660b6b23 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 16 Feb 2016 21:57:25 +0800 Subject: Handle multiple commands on a single line --- gerber/rs274x.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 185cbc3..9d5b141 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -220,8 +220,7 @@ class GerberParser(object): return self.parse_raw(data, filename=None) def parse_raw(self, data, filename=None): - lines = [line for line in StringIO(data)] - for stmt in self._parse(lines): + for stmt in self._parse(self._split_commands(data)): self.evaluate(stmt) self.statements.append(stmt) @@ -230,6 +229,26 @@ class GerberParser(object): stmt.units = self.settings.units return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename) + + def _split_commands(self, data): + """ + Split the data into commands. Commands end with * (and also newline to help with some badly formatted files) + """ + + length = len(data) + start = 0 + + for cur in range(0, length): + + val = data[cur] + if val == '\r' or val == '\n': + if start != cur: + yield data[start:cur] + start = cur + 1 + + elif val == '*': + yield data[start:cur + 1] + start = cur + 1 def dump_json(self): stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} @@ -244,7 +263,7 @@ class GerberParser(object): def _parse(self, data): oldline = '' - for i, line in enumerate(data): + for line in data: line = oldline + line.strip() # skip empty lines -- cgit From 4bc7a6345b16bfeaa969f533a1da97cbf9e44e4c Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 16 Feb 2016 22:24:03 +0800 Subject: Keep aperature macros as single statement. Don't generate regions with no points --- gerber/rs274x.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 9d5b141..92f4c4b 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -237,18 +237,29 @@ class GerberParser(object): length = len(data) start = 0 + in_header = True for cur in range(0, length): - + val = data[cur] + + if val == '%' and start == cur: + in_header = True + continue + if val == '\r' or val == '\n': if start != cur: yield data[start:cur] start = cur + 1 - elif val == '*': + elif not in_header and val == '*': + yield data[start:cur + 1] + start = cur + 1 + + elif in_header and val == '%': yield data[start:cur + 1] start = cur + 1 + in_header = False def dump_json(self): stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} @@ -457,7 +468,9 @@ class GerberParser(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) + # Sometimes we have regions that have no points. Skip those + if self.current_region: + self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) self.current_region = None self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': -- cgit From 02dbc6a51e2ef417f2bd41d6159ba53cc736535d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 21 Feb 2016 10:23:03 +0800 Subject: Additional bounding box calcuation that considers only actual positions, not the movement of the machine --- gerber/rs274x.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 92f4c4b..4ab5472 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -110,6 +110,21 @@ class GerberFile(CamFile): max_y = max(stmt.y, max_y) return ((min_x, max_x), (min_y, max_y)) + + @property + def bounding_box(self): + min_x = min_y = 1000000 + max_x = max_y = -1000000 + + for prim in self.primitives: + bounds = prim.bounding_box + min_x = min(bounds[0][0], min_x) + max_x = max(bounds[0][1], max_x) + + min_y = min(bounds[1][0], min_y) + max_y = max(bounds[1][1], max_y) + + return ((min_x, max_x), (min_y, max_y)) def write(self, filename, settings=None): """ Write data out to a gerber file -- cgit From d274b0823dbc672682c52c5cd649916aedf8f7e4 Mon Sep 17 00:00:00 2001 From: Robert Kirberich Date: Thu, 10 Mar 2016 20:51:53 +0000 Subject: Make sure apertures get a unit --- gerber/rs274x.py | 1 + 1 file changed, 1 insertion(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 76e5101..911b8ae 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -448,6 +448,7 @@ class GerberParser(object): else: aperture = self.macros[shape].build(modifiers) + aperture.units = self.settings.units self.apertures[d] = aperture def _evaluate_mode(self, stmt): -- cgit From a6c186245075efa2af2acf7b736a1c8f0d0d90f6 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 19 Mar 2016 11:28:45 +0800 Subject: Correctly handle empty command statements --- gerber/rs274x.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 4ab5472..692ce71 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -306,6 +306,12 @@ class GerberParser(object): while did_something and len(line) > 0: did_something = False + # consume empty data blocks + if line[0] == '*': + line = line[1:] + did_something = True + continue + # coord (coord, r) = _match_one(self.COORD_STMT, line) if coord: -- cgit From 738bbc7edda0d8006ef4ff8159144ff3cf03d3ba Mon Sep 17 00:00:00 2001 From: Qau Lau Date: Tue, 22 Mar 2016 17:30:20 +0800 Subject: Update rs274x.py python 2.6 bug re incompatibility in sre, see https://bugs.python.org/issue214033 --- gerber/rs274x.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 692ce71..742fddd 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -168,11 +168,11 @@ class GerberParser(object): FS = r"(?PFS)(?P(L|T|D))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" MO = r"(?PMO)(?P(MM|IN))" LP = r"(?PLP)(?P(D|C))" - AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,]?(?P[^,%]*)?" + 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) + AD_MACRO = r"(?PAD)D(?P\d+)(?P{name})[,]?(?P[^,%]*)".format(name=NAME) AM = r"(?PAM)(?P{name})\*(?P[^%]*)".format(name=NAME) # begin deprecated -- cgit From 82fed203100f99aa5df16897f874d8600df85b6e Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 26 Mar 2016 17:14:47 +0800 Subject: D02 in the middle of a region starts a new region --- gerber/rs274x.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 692ce71..2cfef87 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -540,6 +540,7 @@ class GerberParser(object): # from gerber spec revision J3, Section 4.5, page 55: # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. # The current aperture is associated with the region. This has no graphical effect, but allows all its attributes to be applied to the region. + if self.current_region is None: self.current_region = [Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),] else: @@ -557,7 +558,12 @@ class GerberParser(object): self.current_region.append(Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) elif self.op == "D02" or self.op == "D2": - pass + + if self.region_mode == "on": + # D02 in the middle of a region finishes that region and starts a new one + if self.current_region and len(self.current_region) > 1: + self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = None elif self.op == "D03" or self.op == "D3": primitive = copy.deepcopy(self.apertures[self.aperture]) -- cgit From af86c5c5a228855f40ecbf02074bbec65fd9b6d1 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 23 Apr 2016 13:32:32 +0800 Subject: Correctly find the center for single quadrant arcs --- gerber/rs274x.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 7b3a3b9..86785bc 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -21,6 +21,7 @@ import copy import json import re +import sys try: from cStringIO import StringIO @@ -30,6 +31,7 @@ except(ImportError): from .gerber_statements import * from .primitives import * from .cam import CamFile, FileSettings +from .utils import sq_distance def read(filename): @@ -548,7 +550,7 @@ class GerberParser(object): else: i = 0 if stmt.i is None else stmt.i j = 0 if stmt.j is None else stmt.j - center = (start[0] + i, start[1] + j) + center = self._find_center(start, end, (i, j)) if self.region_mode == 'off': self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) else: @@ -579,6 +581,35 @@ class GerberParser(object): self.primitives.append(primitive) self.x, self.y = x, y + + def _find_center(self, start, end, offsets): + """ + In single quadrant mode, the offsets are always positive, which means there are 4 possible centers. + The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees + """ + + if self.quadrant_mode == 'single-quadrant': + + # The Gerber spec says single quadrant only has one possible center, and you can detect + # based on the angle. But for real files, this seems to work better - there is usually + # only one option that makes sense for the center (since the distance should be the same + # from start and end). Find the center that makes the most sense + sqdist_diff_min = sys.maxint + center = None + for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: + + test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1]) + + sqdist_start = sq_distance(start, test_center) + sqdist_end = sq_distance(end, test_center) + + if abs(sqdist_start - sqdist_end) < sqdist_diff_min: + center = test_center + sqdist_diff_min = abs(sqdist_start - sqdist_end) + + return center + else: + return (start[0] + offsets[0], start[1] + offsets[1]) def _evaluate_aperture(self, stmt): self.aperture = stmt.d -- cgit From 5a20b2b92dc7ab82e1f196d1efbf4bb52a163720 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 28 May 2016 14:14:49 +0800 Subject: Fix converting amgroup units --- gerber/rs274x.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 86785bc..2bc8f9a 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -373,7 +373,9 @@ class GerberParser(object): elif param["param"] == "AD": yield ADParamStmt.from_dict(param) elif param["param"] == "AM": - yield AMParamStmt.from_dict(param) + stmt = AMParamStmt.from_dict(param) + stmt.units = self.settings.units + yield stmt elif param["param"] == "OF": yield OFParamStmt.from_dict(param) elif param["param"] == "IN": -- cgit From 6e014c6117d8697639a4897af8fc3576dba1a8e6 Mon Sep 17 00:00:00 2001 From: "visualgui823@live.com" Date: Fri, 3 Jun 2016 10:45:18 +0000 Subject: compliant fs format as FS[Nn][Gn][Dn][Mn] --- gerber/rs274x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 2bc8f9a..7eba1c2 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -167,7 +167,7 @@ class GerberParser(object): STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+" - FS = r"(?PFS)(?P(L|T|D))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" + FS = r"(?PFS)(?P(L|T|D))?(?P(A|I))[NG0-9]*X(?P[0-7][0-7])Y(?P[0-7][0-7])[DM0-9]*" MO = r"(?PMO)(?P(MM|IN))" LP = r"(?PLP)(?P(D|C))" AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,]?(?P[^,%]*)" -- cgit From ccb6eb7a766bd6edf314978f3ec4fc0dcd61652d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 25 Jun 2016 16:46:44 +0800 Subject: Add support for polygon apertures --- gerber/rs274x.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 7eba1c2..ffac66d 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -483,8 +483,18 @@ class GerberParser(object): height = modifiers[0][1] aperture = Obround(position=None, width=width, height=height, units=self.settings.units) elif shape == 'P': - # FIXME: not supported yet? - pass + outer_diameter = modifiers[0][0] + number_vertices = int(modifiers[0][1]) + if len(modifiers[0]) > 2: + rotation = modifiers[0][2] + else: + rotation = 0 + + if len(modifiers[0]) > 3: + hole_diameter = modifiers[0][3] + else: + hole_diameter = 0 + aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_radius=hole_diameter/2.0, rotation=rotation) else: aperture = self.macros[shape].build(modifiers) -- cgit From b140f5e4767912110f69cbda8417a8e076345b70 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 28 Jun 2016 23:15:20 +0800 Subject: Don't flash G03-only commands --- gerber/rs274x.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index ffac66d..384d498 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -536,6 +536,12 @@ class GerberParser(object): elif stmt.function in ('G02', 'G2', 'G03', 'G3'): self.interpolation = 'arc' self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') + + if stmt.only_function: + # Sometimes we get a coordinate statement + # that only sets the function. If so, don't + # try futher otherwise that might draw/flash something + return if stmt.op: self.op = stmt.op -- cgit From 7cd6acf12670f3773113f67ed2acb35cb21c32a0 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 24 Jul 2016 17:08:47 +0800 Subject: Add many render tests based on the Umaco gerger specification. Fix multiple rendering bugs, especially related to holes in flashed apertures --- gerber/rs274x.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 260fbf8..e88bba7 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -482,15 +482,33 @@ class GerberParser(object): aperture = None if shape == 'C': diameter = modifiers[0][0] - aperture = Circle(position=None, diameter=diameter, units=self.settings.units) + + if len(modifiers[0]) >= 2: + hole_diameter = modifiers[0][1] + else: + hole_diameter = 0 + + aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'R': width = modifiers[0][0] height = modifiers[0][1] - aperture = Rectangle(position=None, width=width, height=height, units=self.settings.units) + + if len(modifiers[0]) >= 3: + hole_diameter = modifiers[0][2] + else: + hole_diameter = 0 + + aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'O': width = modifiers[0][0] height = modifiers[0][1] - aperture = Obround(position=None, width=width, height=height, units=self.settings.units) + + if len(modifiers[0]) >= 3: + hole_diameter = modifiers[0][2] + else: + hole_diameter = 0 + + aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'P': outer_diameter = modifiers[0][0] number_vertices = int(modifiers[0][1]) -- cgit From 965d3ce23b92f8aff1063debd6d3364de15791fe Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 24 Jul 2016 22:08:31 +0800 Subject: Add more tests for rendering to PNG. Start adding tests for rendering to Gerber format. Changed definition of no hole to use None instead of 0 so we can differentiate when writing to Gerber format. Makde polygon use hole diameter instead of hole radius to match other primitives --- gerber/rs274x.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index e88bba7..f009232 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -486,7 +486,7 @@ class GerberParser(object): if len(modifiers[0]) >= 2: hole_diameter = modifiers[0][1] else: - hole_diameter = 0 + hole_diameter = None aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'R': @@ -496,7 +496,7 @@ class GerberParser(object): if len(modifiers[0]) >= 3: hole_diameter = modifiers[0][2] else: - hole_diameter = 0 + hole_diameter = None aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'O': @@ -506,7 +506,7 @@ class GerberParser(object): if len(modifiers[0]) >= 3: hole_diameter = modifiers[0][2] else: - hole_diameter = 0 + hole_diameter = None aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'P': @@ -520,8 +520,8 @@ class GerberParser(object): if len(modifiers[0]) > 3: hole_diameter = modifiers[0][3] else: - hole_diameter = 0 - aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_radius=hole_diameter/2.0, rotation=rotation) + hole_diameter = None + aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation) else: aperture = self.macros[shape].build(modifiers) -- cgit From 8cd842a41a55ab3d8f558a2e3e198beba7da58a1 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Manually mere rendering changes --- gerber/rs274x.py | 55 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 15 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index f009232..7fec64f 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -200,7 +200,8 @@ class GerberParser(object): DEPRECATED_FORMAT = re.compile(r'(?PG9[01])\*') # end deprecated - PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) + PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, + AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] @@ -418,7 +419,8 @@ class GerberParser(object): # deprecated codes (deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line) if deprecated_unit: - stmt = MOParamStmt(param="MO", mo="inch" if "G70" in deprecated_unit["mode"] else "metric") + stmt = MOParamStmt(param="MO", mo="inch" if "G70" in + deprecated_unit["mode"] else "metric") self.settings.units = stmt.mode yield stmt line = r @@ -532,7 +534,9 @@ class GerberParser(object): if self.region_mode == 'on' and stmt.mode == 'off': # Sometimes we have regions that have no points. Skip those if self.current_region: - self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = None self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': @@ -562,7 +566,8 @@ class GerberParser(object): self.interpolation = 'linear' elif stmt.function in ('G02', 'G2', 'G03', 'G3'): self.interpolation = 'arc' - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') + self.direction = ('clockwise' if stmt.function in + ('G02', 'G2') else 'counterclockwise') if stmt.only_function: # Sometimes we get a coordinate statement @@ -582,16 +587,30 @@ class GerberParser(object): if self.interpolation == 'linear': if self.region_mode == 'off': - self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Line(start, end, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) else: # from gerber spec revision J3, Section 4.5, page 55: # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. - # The current aperture is associated with the region. This has no graphical effect, but allows all its attributes to be applied to the region. + + # The current aperture is associated with the region. + # This has no graphical effect, but allows all its attributes to + # be applied to the region. if self.current_region is None: - self.current_region = [Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),] - else: - self.current_region.append(Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = [Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units), ] + else: + self.current_region.append(Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units)) else: i = 0 if stmt.i is None else stmt.i j = 0 if stmt.j is None else stmt.j @@ -614,17 +633,23 @@ class GerberParser(object): elif self.op == "D03" or self.op == "D3": primitive = copy.deepcopy(self.apertures[self.aperture]) - # XXX: temporary fix because there are no primitives for Macros and Polygon + + if primitive is not None: - # XXX: just to make it easy to spot - if isinstance(primitive, type([])): - print(primitive[0].to_gerber()) - else: + + if not isinstance(primitive, AMParamStmt): primitive.position = (x, y) primitive.level_polarity = self.level_polarity primitive.units = self.settings.units self.primitives.append(primitive) - + else: + # Aperture Macro + for am_prim in primitive.primitives: + renderable = am_prim.to_primitive((x, y), + self.level_polarity, + self.settings.units) + if renderable is not None: + self.primitives.append(renderable) self.x, self.y = x, y def _find_center(self, start, end, offsets): -- cgit From 5af19af190c1fb0f0c5be029d46d63e657dde4d9 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Commit partial merge so I can work on the plane --- gerber/rs274x.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 7fec64f..e84c161 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -95,6 +95,7 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ + def __init__(self, statements, settings, primitives, apertures, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, filename) @@ -568,7 +569,7 @@ class GerberParser(object): self.interpolation = 'arc' self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') - + if stmt.only_function: # Sometimes we get a coordinate statement # that only sets the function. If so, don't @@ -594,7 +595,6 @@ class GerberParser(object): else: # from gerber spec revision J3, Section 4.5, page 55: # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. - # The current aperture is associated with the region. # This has no graphical effect, but allows all its attributes to # be applied to the region. @@ -616,12 +616,24 @@ class GerberParser(object): j = 0 if stmt.j is None else stmt.j center = self._find_center(start, end, (i, j)) if self.region_mode == 'off': - self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Arc(start, end, center, self.direction, + self.apertures[self.aperture], + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units)) else: if self.current_region is None: - self.current_region = [Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units),] + self.current_region = [Arc(start, end, center, self.direction, + self.apertures.get(self.aperture, Circle((0,0), 0)), + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units),] else: - self.current_region.append(Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region.append(Arc(start, end, center, self.direction, + self.apertures.get(self.aperture, Circle((0,0), 0)), + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units)) elif self.op == "D02" or self.op == "D2": -- cgit From 724c2b3bced319ed0b50c4302fed9b0e1aa9ce9c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 5 Nov 2016 20:56:47 -0400 Subject: Finish Merge, most tests passing --- gerber/rs274x.py | 57 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 27 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index e84c161..2f8dfd2 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -98,7 +98,7 @@ class GerberFile(CamFile): def __init__(self, statements, settings, primitives, apertures, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, filename) - + self.apertures = apertures @property @@ -115,15 +115,18 @@ class GerberFile(CamFile): def bounds(self): min_x = min_y = 1000000 max_x = max_y = -1000000 + for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]: if stmt.x is not None: min_x = min(stmt.x, min_x) max_x = max(stmt.x, max_x) + if stmt.y is not None: min_y = min(stmt.y, min_y) max_y = max(stmt.y, max_y) + return ((min_x, max_x), (min_y, max_y)) - + @property def bounding_box(self): min_x = min_y = 1000000 @@ -258,7 +261,7 @@ class GerberParser(object): stmt.units = self.settings.units return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename) - + def _split_commands(self, data): """ Split the data into commands. Commands end with * (and also newline to help with some badly formatted files) @@ -267,24 +270,24 @@ class GerberParser(object): length = len(data) start = 0 in_header = True - + for cur in range(0, length): val = data[cur] - + if val == '%' and start == cur: in_header = True continue - + if val == '\r' or val == '\n': if start != cur: yield data[start:cur] start = cur + 1 - + elif not in_header and val == '*': yield data[start:cur + 1] start = cur + 1 - + elif in_header and val == '%': yield data[start:cur + 1] start = cur + 1 @@ -318,13 +321,13 @@ class GerberParser(object): did_something = True # make sure we do at least one loop while did_something and len(line) > 0: did_something = False - + # consume empty data blocks if line[0] == '*': line = line[1:] did_something = True continue - + # coord (coord, r) = _match_one(self.COORD_STMT, line) if coord: @@ -332,7 +335,7 @@ class GerberParser(object): line = r did_something = True continue - + # aperture selection (aperture, r) = _match_one(self.APERTURE_STMT, line) if aperture: @@ -485,32 +488,32 @@ class GerberParser(object): aperture = None if shape == 'C': diameter = modifiers[0][0] - + if len(modifiers[0]) >= 2: hole_diameter = modifiers[0][1] else: hole_diameter = None - + aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'R': width = modifiers[0][0] height = modifiers[0][1] - + if len(modifiers[0]) >= 3: hole_diameter = modifiers[0][2] else: hole_diameter = None - + aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'O': width = modifiers[0][0] height = modifiers[0][1] - + if len(modifiers[0]) >= 3: hole_diameter = modifiers[0][2] else: hole_diameter = None - + aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'P': outer_diameter = modifiers[0][0] @@ -519,7 +522,7 @@ class GerberParser(object): rotation = modifiers[0][2] else: rotation = 0 - + if len(modifiers[0]) > 3: hole_diameter = modifiers[0][3] else: @@ -636,7 +639,7 @@ class GerberParser(object): units=self.settings.units)) elif self.op == "D02" or self.op == "D2": - + if self.region_mode == "on": # D02 in the middle of a region finishes that region and starts a new one if self.current_region and len(self.current_region) > 1: @@ -663,32 +666,32 @@ class GerberParser(object): if renderable is not None: self.primitives.append(renderable) self.x, self.y = x, y - + def _find_center(self, start, end, offsets): """ In single quadrant mode, the offsets are always positive, which means there are 4 possible centers. The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees """ - + if self.quadrant_mode == 'single-quadrant': - - # The Gerber spec says single quadrant only has one possible center, and you can detect + + # The Gerber spec says single quadrant only has one possible center, and you can detect # based on the angle. But for real files, this seems to work better - there is usually # only one option that makes sense for the center (since the distance should be the same # from start and end). Find the center that makes the most sense sqdist_diff_min = sys.maxint center = None for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: - + test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1]) - + sqdist_start = sq_distance(start, test_center) sqdist_end = sq_distance(end, test_center) - + if abs(sqdist_start - sqdist_end) < sqdist_diff_min: center = test_center sqdist_diff_min = abs(sqdist_start - sqdist_end) - + return center else: return (start[0] + offsets[0], start[1] + offsets[1]) -- cgit From 6b672e98ff36b25e289c0d9e2ccc28337baa3c27 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:02:22 -0500 Subject: Add support for IF (Include File) rs274x command --- gerber/rs274x.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 5d64597..ff8addd 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -20,6 +20,7 @@ import copy import json +import os import re import sys @@ -146,7 +147,7 @@ class GerberFile(CamFile): return ((min_x, max_x), (min_y, max_y)) def write(self, filename, settings=None): - """ Write data out to a gerber file + """ Write data out to a gerber file. """ with open(filename, 'w') as f: for statement in self.statements: @@ -193,6 +194,9 @@ class GerberParser(object): 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) + # Include File + IF = r"(?PIF)(?P.*)" + # begin deprecated AS = r"(?PAS)(?P(AXBY)|(AYBX))" @@ -208,7 +212,7 @@ class GerberParser(object): # end deprecated PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, - AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) + AD_MACRO, AM, AS, IF, IN, IP, IR, MI, OF, SF, LN) PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] @@ -230,7 +234,11 @@ class GerberParser(object): REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') + # Keep include loop from crashing us + INCLUDE_FILE_RECURSION_LIMIT = 10 + def __init__(self): + self.filename = None self.settings = FileSettings() self.statements = [] self.primitives = [] @@ -248,13 +256,16 @@ class GerberParser(object): self.region_mode = 'off' self.quadrant_mode = 'multi-quadrant' self.step_and_repeat = (1, 1, 0, 0) + self._recursion_depth = 0 def parse(self, filename): + self.filename = filename with open(filename, "rU") as fp: data = fp.read() return self.parse_raw(data, filename) def parse_raw(self, data, filename=None): + self.filename = filename for stmt in self._parse(self._split_commands(data)): self.evaluate(stmt) self.statements.append(stmt) @@ -371,6 +382,17 @@ class GerberParser(object): yield stmt elif param["param"] == "OF": yield OFParamStmt.from_dict(param) + elif param["param"] == "IF": + # Don't crash on include loop + if self._recursion_depth < self.INCLUDE_FILE_RECURSION_LIMIT: + self._recursion_depth += 1 + with open(os.path.join(os.path.dirname(self.filename), param["filename"]), 'r') as f: + inc_data = f.read() + for stmt in self._parse(self._split_commands(inc_data)): + yield stmt + self._recursion_depth -= 1 + else: + raise IOError("Include file nesting depth limit exceeded.") elif param["param"] == "IN": yield INParamStmt.from_dict(param) elif param["param"] == "LN": -- cgit From a7f1f6ef0fdd9c792b3234931754dac5d81b15e5 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:05:57 -0500 Subject: Finish adding square hole support, fix some primitive calculations, etc. --- gerber/rs274x.py | 115 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 30 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index ff8addd..5191fb7 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -514,32 +514,51 @@ class GerberParser(object): if shape == 'C': diameter = modifiers[0][0] - if len(modifiers[0]) >= 2: + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 2: hole_diameter = modifiers[0][1] - else: - hole_diameter = None + elif len(modifiers[0]) == 3: + rectangular_hole = modifiers[0][1:3] + + aperture = Circle(position=None, diameter=diameter, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) - aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'R': width = modifiers[0][0] height = modifiers[0][1] - if len(modifiers[0]) >= 3: + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 3: hole_diameter = modifiers[0][2] - else: - hole_diameter = None - - aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) + elif len(modifiers[0]) == 4: + rectangular_hole = modifiers[0][2:4] + + aperture = Rectangle(position=None, width=width, height=height, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) elif shape == 'O': width = modifiers[0][0] height = modifiers[0][1] - if len(modifiers[0]) >= 3: + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 3: hole_diameter = modifiers[0][2] - else: - hole_diameter = None - - aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) + elif len(modifiers[0]) == 4: + rectangular_hole = modifiers[0][2:4] + + aperture = Obround(position=None, width=width, height=height, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) elif shape == 'P': outer_diameter = modifiers[0][0] number_vertices = int(modifiers[0][1]) @@ -548,11 +567,19 @@ class GerberParser(object): else: rotation = 0 - if len(modifiers[0]) > 3: + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 4: hole_diameter = modifiers[0][3] - else: - hole_diameter = None - aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation) + elif len(modifiers[0]) >= 5: + rectangular_hole = modifiers[0][3:5] + + aperture = Polygon(position=None, sides=number_vertices, + radius=outer_diameter/2.0, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + rotation=rotation) else: aperture = self.macros[shape].build(modifiers) @@ -663,13 +690,18 @@ class GerberParser(object): quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) + # Gerbv seems to reset interpolation mode in regions.. + # TODO: Make sure this is right. + self.interpolation = 'linear' elif self.op == "D02" or self.op == "D2": if self.region_mode == "on": # D02 in the middle of a region finishes that region and starts a new one if self.current_region and len(self.current_region) > 1: - self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity, + units=self.settings.units)) self.current_region = None elif self.op == "D03" or self.op == "D3": @@ -694,29 +726,53 @@ class GerberParser(object): def _find_center(self, start, end, offsets): """ - In single quadrant mode, the offsets are always positive, which means there are 4 possible centers. - The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees + In single quadrant mode, the offsets are always positive, which means + there are 4 possible centers. The correct center is the only one that + results in an arc with sweep angle of less than or equal to 90 degrees + in the specified direction """ - + two_pi = 2 * math.pi if self.quadrant_mode == 'single-quadrant': + # The Gerber spec says single quadrant only has one possible center, + # and you can detect it based on the angle. But for real files, this + # seems to work better - there is usually only one option that makes + # sense for the center (since the distance should be the same + # from start and end). We select the center with the least error in + # radius from all the options with a valid sweep angle. - # The Gerber spec says single quadrant only has one possible center, and you can detect - # based on the angle. But for real files, this seems to work better - there is usually - # only one option that makes sense for the center (since the distance should be the same - # from start and end). Find the center that makes the most sense sqdist_diff_min = sys.maxint center = None for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: - test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1]) + test_center = (start[0] + offsets[0] * factors[0], + start[1] + offsets[1] * factors[1]) + + # Find angle from center to start and end points + start_angle = math.atan2(*reversed([_start - _center for _start, _center in zip(start, test_center)])) + end_angle = math.atan2(*reversed([_end - _center for _end, _center in zip(end, test_center)])) + # Clamp angles to 0, 2pi + theta0 = (start_angle + two_pi) % two_pi + theta1 = (end_angle + two_pi) % two_pi + + # Determine sweep angle in the current arc direction + if self.direction == 'counterclockwise': + sweep_angle = abs(theta1 - theta0) + else: + theta0 += two_pi + sweep_angle = abs(theta0 - theta1) % two_pi + + # Calculate the radius error sqdist_start = sq_distance(start, test_center) sqdist_end = sq_distance(end, test_center) - if abs(sqdist_start - sqdist_end) < sqdist_diff_min: + # Take the option with the lowest radius error from the set of + # options with a valid sweep angle + if ((abs(sqdist_start - sqdist_end) < sqdist_diff_min) + and (sweep_angle >= 0) + and (sweep_angle <= math.pi / 2.0)): center = test_center sqdist_diff_min = abs(sqdist_start - sqdist_end) - return center else: return (start[0] + offsets[0], start[1] + offsets[1]) @@ -724,7 +780,6 @@ class GerberParser(object): def _evaluate_aperture(self, stmt): self.aperture = stmt.d - def _match_one(expr, data): match = expr.match(data) if match is None: -- cgit From 5b67c82abc70a2ca76ff21fa706b045259c26a15 Mon Sep 17 00:00:00 2001 From: Jan Margeta Date: Sat, 15 Apr 2017 11:13:31 +0200 Subject: Replace sys.maxint with sys.maxsize In Python 3, sys.maxint was removed, however its current use can be safely substituted with sys.maxsize (also in Python 2) See also: https://docs.python.org/3.1/whatsnew/3.0.html#integers --- gerber/rs274x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 5191fb7..813b246 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -740,7 +740,7 @@ class GerberParser(object): # from start and end). We select the center with the least error in # radius from all the options with a valid sweep angle. - sqdist_diff_min = sys.maxint + sqdist_diff_min = sys.maxsize center = None for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: -- cgit From dd1676ad8a9fff65231c22c01efc4bbccc21eb05 Mon Sep 17 00:00:00 2001 From: Jan Margeta Date: Sat, 15 Apr 2017 15:40:53 +0200 Subject: Add tolerance to center finding In some cases, the computation of valid sweep angle hit numerical limits and no centers are found. This commit adds a small amount of tolerance. --- gerber/rs274x.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'gerber/rs274x.py') diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 5191fb7..fee60b3 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -765,14 +765,17 @@ class GerberParser(object): # Calculate the radius error sqdist_start = sq_distance(start, test_center) sqdist_end = sq_distance(end, test_center) + sqdist_diff = abs(sqdist_start - sqdist_end) # Take the option with the lowest radius error from the set of # options with a valid sweep angle - if ((abs(sqdist_start - sqdist_end) < sqdist_diff_min) - and (sweep_angle >= 0) - and (sweep_angle <= math.pi / 2.0)): + # In some rare cases, the sweep angle is numerically (10**-14) above pi/2 + # So it is safer to compare the angles with some tolerance + is_lowest_radius_error = sqdist_diff < sqdist_diff_min + is_valid_sweep_angle = sweep_angle >= 0 and sweep_angle <= math.pi / 2.0 + 1e-6 + if is_lowest_radius_error and is_valid_sweep_angle: center = test_center - sqdist_diff_min = abs(sqdist_start - sqdist_end) + sqdist_diff_min = sqdist_diff return center else: return (start[0] + offsets[0], start[1] + offsets[1]) -- cgit