From 3a5dbcf1e13704b7352d5fb3c4777d7df3fed081 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 28 Sep 2014 21:17:13 -0400 Subject: added ExcellonFile class --- Makefile | 9 +++ gerber/__init__.py | 15 ++++ gerber/__main__.py | 10 +-- gerber/excellon.py | 133 +++++++++++++++++++++----------- gerber/gerber.py | 77 +++++++++---------- gerber/render/apertures.py | 9 ++- gerber/render/render.py | 12 +-- gerber/render/svg.py | 50 ++++++------ gerber/statements.py | 187 ++++++++++++++++++++++----------------------- gerber/utils.py | 108 ++++++++++++++++---------- 10 files changed, 345 insertions(+), 265 deletions(-) diff --git a/Makefile b/Makefile index 3a5e411..162094c 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ PYTHON ?= python NOSETESTS ?= nosetests +DOC_ROOT = doc clean: #$(PYTHON) setup.py clean @@ -15,3 +16,11 @@ test-coverage: rm -rf coverage .coverage $(NOSETESTS) -s -v --with-coverage gerber +doc-html: + (cd $(DOC_ROOT); make html) + + +doc-clean: + (cd $(DOC_ROOT); make clean) + + diff --git a/gerber/__init__.py b/gerber/__init__.py index 0bf7c24..089d7b6 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -14,3 +14,18 @@ # 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. + + +def read(filename): + """ Read a gerber or excellon file and return a representative object. + """ + import gerber + import excellon + from utils import detect_file_format + fmt = detect_file_format(filename) + if fmt == 'rs274x': + return gerber.read(filename) + elif fmt == 'excellon': + return excellon.read(filename) + else: + return None diff --git a/gerber/__main__.py b/gerber/__main__.py index d32fa01..31b70f8 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -10,10 +10,10 @@ # 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. +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. if __name__ == '__main__': from .gerber import GerberFile @@ -34,5 +34,3 @@ if __name__ == '__main__': p = ExcellonParser(ctx) p.parse('ncdrill.txt') p.dump('testwithdrill.svg') - - diff --git a/gerber/excellon.py b/gerber/excellon.py index fef5844..d92d57c 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -2,19 +2,60 @@ import re from itertools import tee, izip from .utils import parse_gerber_value - - -INCH = 0 -METRIC = 1 -ABSOLUTE = 0 -INCREMENTAL = 1 -LZ = 0 -TZ = 1 +def read(filename): + """ Read data from filename and return an ExcellonFile + """ + return ExcellonParser().parse(filename) -class Tool(object): +class ExcellonFile(object): + """ A class representing a single excellon file + + The ExcellonFile class represents a single excellon file. + + Parameters + ---------- + tools : list + list of gerber file statements + + hits : list of tuples + list of drill hits as (, (x, y)) + settings : dict + Dictionary of gerber file settings + + filename : string + Filename of the source gerber file + + Attributes + ---------- + units : string + either 'inch' or 'metric'. + + """ + def __init__(self, tools, hits, settings, filename): + self.tools = tools + self.hits = hits + self.settings = settings + self.filename = filename + + def report(self): + """ Print drill report + """ + pass + + def render(self, filename, ctx): + """ Generate image of file + """ + for tool, pos in self.hits: + ctx.drill(pos[0], pos[1], tool.diameter) + ctx.dump(filename) + + +class Tool(object): + """ Excellon Tool class + """ @classmethod def from_line(cls, line, settings): commands = re.split('([BCFHSTZ])', line)[1:] @@ -38,7 +79,7 @@ class Tool(object): elif cmd == 'Z': args['depth_offset'] = parse_gerber_value(val, format, zero_suppression) return cls(settings, **args) - + def __init__(self, settings, **kwargs): self.number = kwargs.get('number') self.feed_rate = kwargs.get('feed_rate') @@ -47,79 +88,83 @@ class Tool(object): self.diameter = kwargs.get('diameter') self.max_hit_count = kwargs.get('max_hit_count') self.depth_offset = kwargs.get('depth_offset') - self.units = settings.get('units', INCH) - + self.units = settings.get('units', 'inch') + def __repr__(self): - unit = 'in.' if self.units == INCH else 'mm' + unit = 'in.' if self.units == 'inch' else 'mm' return '' % (self.number, self.diameter, unit) - class ExcellonParser(object): def __init__(self, ctx=None): - self.ctx=ctx + self.ctx = ctx self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' - self.format = (2,5) + self.format = (2, 5) self.state = 'INIT' - self.tools = {} + self.tools = [] self.hits = [] self.active_tool = None self.pos = [0., 0.] if ctx is not None: - self.ctx.set_coord_format(zero_suppression='trailing', format=[2,5], notation='absolute') + self.ctx.set_coord_format(zero_suppression='trailing', + format=(2, 5), notation='absolute') + def parse(self, filename): with open(filename, 'r') as f: for line in f: self._parse(line) - + settings = {'notation': self.notation, 'units': self.units, + 'zero_suppression': self.zero_suppression, + 'format': self.format} + return ExcellonFile(self.tools, self.hits, settings, filename) + def dump(self, filename): self.ctx.dump(filename) - + def _parse(self, line): if 'M48' in line: self.state = 'HEADER' - + if 'G00' in line: self.state = 'ROUT' - + if 'G05' in line: self.state = 'DRILL' - + elif line[0] == '%' and self.state == 'HEADER': self.state = 'DRILL' - + if 'INCH' in line or line.strip() == 'M72': - self.units = 'INCH' - + self.units = 'inch' + elif 'METRIC' in line or line.strip() == 'M71': - self.units = 'METRIC' - + self.units = 'metric' + if 'LZ' in line: self.zeros = 'L' - + elif 'TZ' in line: self.zeros = 'T' if 'ICI' in line and 'ON' in line or line.strip() == 'G91': self.notation = 'incremental' - + if 'ICI' in line and 'OFF' in line or line.strip() == 'G90': self.notation = 'incremental' - + zs = self._settings()['zero_suppression'] fmt = self._settings()['format'] - + # tool definition if line[0] == 'T' and self.state == 'HEADER': - tool = Tool.from_line(line,self._settings()) + tool = Tool.from_line(line, self._settings()) self.tools[tool.number] = tool - + elif line[0] == 'T' and self.state != 'HEADER': self.active_tool = self.tools[int(line.strip().split('T')[1])] - if line[0] in ['X', 'Y']: x = None y = None @@ -127,10 +172,9 @@ class ExcellonParser(object): splitline = line.strip('X').split('Y') x = parse_gerber_value(splitline[0].strip(), fmt, zs) if len(splitline) == 2: - y = parse_gerber_value(splitline[1].strip(), fmt,zs) + y = parse_gerber_value(splitline[1].strip(), fmt, zs) else: - y = parse_gerber_value(line.strip(' Y'), fmt,zs) - + y = parse_gerber_value(line.strip(' Y'), fmt, zs) if self.notation == 'absolute': if x is not None: self.pos[0] = x @@ -146,20 +190,17 @@ class ExcellonParser(object): if self.ctx is not None: self.ctx.drill(self.pos[0], self.pos[1], self.active_tool.diameter) - + def _settings(self): - return {'units':self.units, 'zero_suppression':self.zero_suppression, + return {'units': self.units, 'zero_suppression': self.zero_suppression, 'format': self.format} - + + def pairwise(iterator): itr = iter(iterator) while True: yield tuple([itr.next() for i in range(2)]) - + if __name__ == '__main__': - tools = [] - settings = {'units':INCH, 'zeros':LZ} p = parser() p.parse('examples/ncdrill.txt') - - \ No newline at end of file diff --git a/gerber/gerber.py b/gerber/gerber.py index 31d9b82..949037b 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -3,7 +3,7 @@ # 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 @@ -15,33 +15,41 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +gerber.gerber +============ +**Gerber File module** + +This module provides an RS-274-X class and parser +""" + import re import json from .statements import * +def read(filename): + """ Read data from filename and return a GerberFile + """ + return GerberParser().parse(filename) class GerberFile(object): """ A class representing a single gerber file - - The GerberFile class represents 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 - Parameter. - - zero_suppression : string - Zero-suppression mode. May be either 'leading' or 'trailing' - - notation : string - Notation mode. May be either 'absolute' or 'incremental' - - format : tuple (int, int) - Gerber precision format expressed as a tuple containing: - (number of integer-part digits, number of decimal-part digits) + Filename of the source gerber file Attributes ---------- @@ -50,7 +58,7 @@ class GerberFile(object): units : string either 'inch' or 'metric'. - + size : tuple, (, ) Size in [self.units] of the layer described by the gerber file. @@ -59,32 +67,25 @@ class GerberFile(object): `bounds` is stored as ((min x, max x), (min y, max y)) """ - - @classmethod - def read(cls, filename): - """ Read data from filename and return a GerberFile - """ - return GerberParser().parse(filename) - def __init__(self, statements, settings, filename=None): self.filename = filename self.statements = statements self.settings = settings - + @property def comments(self): return [comment.comment for comment in self.statements if isinstance(comment, CommentStmt)] - + @property def units(self): return self.settings['units'] - + @property def size(self): xbounds, ybounds = self.bounds return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) - + @property def bounds(self): xbounds = [0.0, 0.0] @@ -106,9 +107,8 @@ class GerberFile(object): ybounds[0] = stmt.j if stmt.j is not None and stmt.j > ybounds[1]: ybounds[1] = stmt.j - - return (xbounds, ybounds) - + return (xbounds, ybounds) + def write(self, filename): """ Write data out to a gerber file """ @@ -123,8 +123,7 @@ class GerberFile(object): for statement in self.statements: ctx.evaluate(statement) ctx.dump(filename) - - + class GerberParser(object): """ GerberParser @@ -179,7 +178,7 @@ class GerberParser(object): for stmt in self._parse(data): self.statements.append(stmt) - + return GerberFile(self.statements, self.settings, filename) def dump_json(self): @@ -197,7 +196,7 @@ class GerberParser(object): for i, line in enumerate(data): line = oldline + line.strip() - + # skip empty lines if not len(line): continue @@ -207,10 +206,10 @@ class GerberParser(object): oldline = line continue - did_something = True # make sure we do at least one loop + did_something = True # make sure we do at least one loop while did_something and len(line) > 0: did_something = False - + # coord (coord, r) = self._match_one(self.COORD_STMT, line) if coord: @@ -223,7 +222,7 @@ class GerberParser(object): (aperture, r) = self._match_one(self.APERTURE_STMT, line) if aperture: yield ApertureStmt(**aperture) - + did_something = True line = r continue @@ -240,7 +239,7 @@ class GerberParser(object): (param, r) = self._match_one_from_many(self.PARAM_STMT, line) if param: if param["param"] == "FS": - stmt = FSParamStmt.from_dict(param) + stmt = FSParamStmt.from_dict(param) self.settings = {'zero_suppression': stmt.zero_suppression, 'format': stmt.format, 'notation': stmt.notation} @@ -276,7 +275,7 @@ class GerberParser(object): did_something = True line = r continue - + if False: print self.COORD_STMT.pattern print self.APERTURE_STMT.pattern diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py index 55e6a30..f163b1f 100644 --- a/gerber/render/apertures.py +++ b/gerber/render/apertures.py @@ -29,7 +29,7 @@ class Aperture(object): """ def draw(self, ctx, x, y): raise NotImplementedError('The draw method must be implemented in an Aperture subclass.') - + def flash(self, ctx, x, y): raise NotImplementedError('The flash method must be implemented in an Aperture subclass.') @@ -40,19 +40,22 @@ class Circle(Aperture): def __init__(self, diameter=0.0): self.diameter = diameter + class Rect(Aperture): """ Rectangular Aperture base class """ def __init__(self, size=(0, 0)): self.size = size + class Obround(Aperture): """ Obround Aperture base class """ def __init__(self, size=(0, 0)): self.size = size - + + class Polygon(Aperture): """ Polygon Aperture base class """ - pass \ No newline at end of file + pass diff --git a/gerber/render/render.py b/gerber/render/render.py index e15a36f..c372783 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -34,11 +34,11 @@ class GerberContext(object): level_polarity = 'dark' def __init__(self): - pass + pass def set_format(self, settings): self.settings = settings - + def set_coord_format(self, zero_suppression, format, notation): self.settings['zero_suppression'] = zero_suppression self.settings['format'] = format @@ -52,9 +52,9 @@ class GerberContext(object): def set_image_polarity(self, polarity): self.image_polarity = polarity - + def set_level_polarity(self, polarity): - self.level_polarity = polarity + self.level_polarity = polarity def set_interpolation(self, interpolation): self.interpolation = 'linear' if interpolation in ("G01", "G1") else 'arc' @@ -63,8 +63,8 @@ class GerberContext(object): self.aperture = d def resolve(self, x, y): - return x or self.x, y or self.y - + return x or self.x, y or self.y + def define_aperture(self, d, shape, modifiers): pass diff --git a/gerber/render/svg.py b/gerber/render/svg.py index b16e534..7d5c8fd 100644 --- a/gerber/render/svg.py +++ b/gerber/render/svg.py @@ -33,8 +33,8 @@ class SvgCircle(Circle): def flash(self, ctx, x, y): return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill='rgb(184, 115, 51)'),] + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'), ] class SvgRect(Rect): @@ -47,41 +47,42 @@ class SvgRect(Rect): def flash(self, ctx, x, y): xsize, ysize = self.size return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill="rgb(184, 115, 51)"),] + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"), ] + class SvgObround(Obround): def draw(self, ctx, x, y): pass - + def flash(self, ctx, x, y): xsize, ysize = self.size - + # horizontal obround if xsize == ysize: return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill='rgb(184, 115, 51)'),] + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'), ] if xsize > ysize: rectx = xsize - ysize recty = ysize lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), + -y * SCALE), r = SCALE * (ysize / 2.0), fill='rgb(184, 115, 51)') - + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), + -y * SCALE), r = SCALE * (ysize / 2.0), fill='rgb(184, 115, 51)') - + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill='rgb(184, 115, 51)') - return [lcircle, rcircle, rect,] - + return [lcircle, rcircle, rect, ] + # Vertical obround else: rectx = xsize @@ -90,18 +91,18 @@ class SvgObround(Obround): (y - (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), fill='rgb(184, 115, 51)') - + ucircle = ctx.dwg.circle(center=(x * SCALE, (y + (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), fill='rgb(184, 115, 51)') - + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill='rgb(184, 115, 51)') - return [lcircle, ucircle, rect,] - + return [lcircle, ucircle, rect, ] + class GerberSvgContext(GerberContext): def __init__(self): @@ -112,10 +113,9 @@ class GerberSvgContext(GerberContext): #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) def set_bounds(self, bounds): - xbounds, ybounds = bounds + xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) - def define_aperture(self, d, shape, modifiers): aperture = None @@ -133,8 +133,7 @@ class GerberSvgContext(GerberContext): if self.interpolation == 'linear': self.line(x, y) elif self.interpolation == 'arc': - #self.arc(x, y) - self.line(x,y) + self.arc(x, y) def line(self, x, y): super(GerberSvgContext, self).line(x, y) @@ -145,11 +144,9 @@ class GerberSvgContext(GerberContext): self.dwg.add(ap.draw(self, x, y)) self.move(x, y, resolve=False) - def arc(self, x, y): super(GerberSvgContext, self).arc(x, y) - def flash(self, x, y): super(GerberSvgContext, self).flash(x, y) x, y = self.resolve(x, y) @@ -160,12 +157,9 @@ class GerberSvgContext(GerberContext): self.dwg.add(shape) self.move(x, y, resolve=False) - def drill(self, x, y, diameter): hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') self.dwg.add(hit) def dump(self, filename): self.dwg.saveas(filename) - - diff --git a/gerber/statements.py b/gerber/statements.py index 53f7f78..418a852 100644 --- a/gerber/statements.py +++ b/gerber/statements.py @@ -3,18 +3,18 @@ """ gerber.statements ================= -**Gerber file statement classes ** +**Gerber file statement classes** """ from .utils import parse_gerber_value, write_gerber_value, decimal_string - -__all__ = ['FSParamStmt', 'MOParamStmt','IPParamStmt', 'OFParamStmt', +__all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', 'EofStmt', 'UnknownStmt'] + class Statement(object): def __init__(self, type): self.type = type @@ -38,15 +38,15 @@ class ParamStmt(Statement): class FSParamStmt(ParamStmt): """ FS - Gerber Format Specification Statement """ - + @classmethod def from_dict(cls, stmt_dict): - """ + """ """ param = stmt_dict.get('param').strip() zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' - x = map(int,stmt_dict.get('x').strip()) + x = map(int, stmt_dict.get('x').strip()) format = (x[0], x[1]) if notation == 'incremental': print('This file uses incremental notation. To quote the gerber \ @@ -54,36 +54,36 @@ class FSParamStmt(ParamStmt): endless confusion. Always use absolute notation.\n\nYou \ have been warned') return cls(param, zeros, notation, format) - + def __init__(self, param, zero_suppression='leading', - notation='absolute', format=(2,4)): + notation='absolute', format=(2, 4)): """ Initialize FSParamStmt class - + .. note:: The FS command specifies the format of the coordinate data. It must only be used once at the beginning of a file. It must be specified before the first use of coordinate data. - + Parameters ---------- param : string Parameter. - + zero_suppression : string Zero-suppression mode. May be either 'leading' or 'trailing' notation : string Notation mode. May be either 'absolute' or 'incremental' - + format : tuple (int, int) - Gerber precision format expressed as a tuple containing: + Gerber precision format expressed as a tuple containing: (number of integer-part digits, number of decimal-part digits) Returns ------- ParamStmt : FSParamStmt Initialized FSParamStmt class. - + """ ParamStmt.__init__(self, param) self.zero_suppression = zero_suppression @@ -93,7 +93,7 @@ class FSParamStmt(ParamStmt): def to_gerber(self): zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T' notation = 'A' if self.notation == 'absolute' else 'I' - format = ''.join(map(str,self.format)) + format = ''.join(map(str, self.format)) return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, format, format) @@ -104,23 +104,23 @@ class FSParamStmt(ParamStmt): class MOParamStmt(ParamStmt): - """ MO - Gerber Mode (measurement units) Statement. + """ MO - Gerber Mode (measurement units) Statement. """ - + @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric' return cls(param, mo) - + def __init__(self, param, mo): """ Initialize MOParamStmt class - + Parameters ---------- param : string Parameter. - + mo : string Measurement units. May be either 'inch' or 'metric' @@ -128,11 +128,11 @@ class MOParamStmt(ParamStmt): ------- ParamStmt : MOParamStmt Initialized MOParamStmt class. - + """ ParamStmt.__init__(self, param) self.mode = mo - + def to_gerber(self): mode = 'MM' if self.mode == 'metric' else 'IN' return '%MO{0}*%'.format(mode) @@ -140,7 +140,7 @@ class MOParamStmt(ParamStmt): def __str__(self): mode_str = 'millimeters' if self.mode == 'metric' else 'inches' return ('' % mode_str) - + class IPParamStmt(ParamStmt): """ IP - Gerber Image Polarity Statement. (Deprecated) @@ -150,15 +150,15 @@ class IPParamStmt(ParamStmt): param = stmt_dict.get('param') ip = 'positive' if stmt_dict.get('ip') == 'POS' else 'negative' return cls(param, ip) - + def __init__(self, param, ip): """ Initialize IPParamStmt class - + Parameters ---------- param : string Parameter string. - + ip : string Image polarity. May be either'positive' or 'negative' @@ -166,12 +166,11 @@ class IPParamStmt(ParamStmt): ------- ParamStmt : IPParamStmt Initialized IPParamStmt class. - + """ ParamStmt.__init__(self, param) self.ip = ip - def to_gerber(self): ip = 'POS' if self.ip == 'positive' else 'negative' return '%IP{0}*%'.format(ip) @@ -183,33 +182,33 @@ class IPParamStmt(ParamStmt): class OFParamStmt(ParamStmt): """ OF - Gerber Offset statement (Deprecated) """ - + @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') a = float(stmt_dict.get('a')) b = float(stmt_dict.get('b')) return cls(param, a, b) - + def __init__(self, param, a, b): """ Initialize OFParamStmt class - + Parameters ---------- param : string Parameter - + a : float Offset along the output device A axis b : float Offset along the output device B axis - + Returns ------- ParamStmt : OFParamStmt Initialized OFParamStmt class. - + """ ParamStmt.__init__(self, param) self.a = a @@ -231,24 +230,25 @@ class OFParamStmt(ParamStmt): offset_str += ('Y: %f' % self.b) return ('' % offset_str) + class LPParamStmt(ParamStmt): """ LP - Gerber Level Polarity statement """ - + @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('lp') - lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' + lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' return cls(param, lp) - + def __init__(self, param, lp): """ Initialize LPParamStmt class - + Parameters ---------- param : string Parameter - + lp : string Level polarity. May be either 'clear' or 'dark' @@ -256,12 +256,11 @@ class LPParamStmt(ParamStmt): ------- ParamStmt : LPParamStmt Initialized LPParamStmt class. - + """ ParamStmt.__init__(self, param) self.lp = lp - def to_gerber(self, settings): lp = 'C' if self.lp == 'clear' else 'dark' return '%LP{0}*%'.format(self.lp) @@ -273,7 +272,7 @@ class LPParamStmt(ParamStmt): class ADParamStmt(ParamStmt): """ AD - Gerber Aperture Definition Statement """ - + @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') @@ -282,38 +281,36 @@ class ADParamStmt(ParamStmt): modifiers = stmt_dict.get('modifiers') if modifiers is not None: modifiers = [[float(x) for x in m.split('X')] - for m in modifiers.split(',')] + for m in modifiers.split(',')] return cls(param, d, shape, modifiers) - - + def __init__(self, param, d, shape, modifiers): """ Initialize ADParamStmt class - + Parameters ---------- param : string Parameter code - + d : int Aperture D-code shape : string aperture name - + modifiers : list of lists of floats Shape modifiers - + Returns ------- ParamStmt : LPParamStmt Initialized LPParamStmt class. - + """ ParamStmt.__init__(self, param) self.d = d self.shape = shape - self.modifiers = modifiers - + self.modifiers = modifiers def to_gerber(self, settings): return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, @@ -328,72 +325,72 @@ class ADParamStmt(ParamStmt): shape = 'oblong' else: shape = self.shape - + return '' % (self.d, shape) class AMParamStmt(ParamStmt): """ AM - Aperture Macro Statement """ - + @classmethod def from_dict(cls, stmt_dict): return cls(**stmt_dict) - + def __init__(self, param, name, macro): """ Initialize AMParamStmt class - + Parameters ---------- param : string Parameter code - + name : string Aperture macro name macro : string Aperture macro string - + Returns ------- ParamStmt : AMParamStmt Initialized AMParamStmt class. - + """ ParamStmt.__init__(self, param) self.name = name self.macro = macro - + def to_gerber(self): return '%AM{0}*{1}*%'.format(self.name, self.macro) def __str__(self): return '' % (self.name, macro) - - + + class INParamStmt(ParamStmt): """ IN - Image Name Statement """ @classmethod def from_dict(cls, stmt_dict): return cls(**stmt_dict) - + def __init__(self, param, name): """ Initialize INParamStmt class - + Parameters ---------- param : string Parameter code - + name : string Image name - + Returns ------- ParamStmt : INParamStmt Initialized INParamStmt class. - + """ ParamStmt.__init__(self, param) self.name = name @@ -404,29 +401,30 @@ class INParamStmt(ParamStmt): def __str__(self): return '' % self.name + class LNParamStmt(ParamStmt): """ LN - Level Name Statement (Deprecated) """ @classmethod def from_dict(cls, stmt_dict): return cls(**stmt_dict) - + def __init__(self, param, name): """ Initialize LNParamStmt class - + Parameters ---------- param : string Parameter code - + name : string Level name - + Returns ------- ParamStmt : LNParamStmt Initialized LNParamStmt class. - + """ ParamStmt.__init__(self, param) self.name = name @@ -437,10 +435,11 @@ class LNParamStmt(ParamStmt): def __str__(self): return '' % self.name + class CoordStmt(Statement): """ Coordinate Data Block """ - + @classmethod def from_dict(cls, stmt_dict, settings): zeros = settings['zero_suppression'] @@ -451,7 +450,7 @@ class CoordStmt(Statement): i = stmt_dict.get('i') j = stmt_dict.get('j') op = stmt_dict.get('op') - + if x is not None: x = parse_gerber_value(stmt_dict.get('x'), format, zeros) @@ -465,39 +464,38 @@ class CoordStmt(Statement): j = parse_gerber_value(stmt_dict.get('j'), format, zeros) return cls(function, x, y, i, j, op, settings) - - + def __init__(self, function, x, y, i, j, op, settings): """ Initialize CoordStmt class - + Parameters ---------- function : string function - + x : float X coordinate - + y : float - Y coordinate - + Y coordinate + i : float Coordinate offset in the X direction - + j : float Coordinate offset in the Y direction - + op : string Operation code - + settings : dict {'zero_suppression', 'format'} - Gerber file coordinate format - + Gerber file coordinate format + Returns ------- Statement : CoordStmt Initialized CoordStmt class. - + """ Statement.__init__(self, "COORD") self.zero_suppression = settings['zero_suppression'] @@ -509,7 +507,6 @@ class CoordStmt(Statement): self.j = j self.op = op - def to_gerber(self): ret = '' if self.function: @@ -518,7 +515,7 @@ class CoordStmt(Statement): ret += 'X{0}'.format(write_gerber_value(self.x, self.zeros, self.format)) if self.y: - ret += 'Y{0}'.format(write_gerber_value(self.y,self. zeros, + ret += 'Y{0}'.format(write_gerber_value(self.y, self. zeros, self.format)) if self.i: ret += 'I{0}'.format(write_gerber_value(self.i, self.zeros, @@ -552,7 +549,7 @@ class CoordStmt(Statement): else: op = self.op coord_str += 'Op: %s' % op - + return '' % coord_str @@ -562,20 +559,21 @@ class ApertureStmt(Statement): def __init__(self, d): Statement.__init__(self, "APERTURE") self.d = int(d) - + def to_gerber(self): return 'G54D{0}*'.format(self.d) def __str__(self): return '' % self.d + class CommentStmt(Statement): """ Comment Statment """ def __init__(self, comment): Statement.__init__(self, "COMMENT") self.comment = comment - + def to_gerber(self): return 'G04{0}*'.format(self.comment) @@ -594,12 +592,11 @@ class EofStmt(Statement): def __str__(self): return '' - - + + class UnknownStmt(Statement): """ Unknown Statement """ def __init__(self, line): Statement.__init__(self, "UNKNOWN") self.line = line - \ No newline at end of file diff --git a/gerber/utils.py b/gerber/utils.py index 35b4fd0..625a9e1 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -10,28 +10,29 @@ files. """ # Author: Hamilton Kibbe -# License: +# License: + def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert gerber/excellon formatted string to floating-point number - + .. note:: - Format and zero suppression are configurable. Note that the Excellon - and Gerber formats use opposite terminology with respect to leading - and trailing zeros. The Gerber format specifies which zeros are - suppressed, while the Excellon format specifies which zeros are - included. This function uses the Gerber-file convention, so an - Excellon file in LZ (leading zeros) mode would use - `zero_suppression='trailing'` - - + Format and zero suppression are configurable. Note that the Excellon + and Gerber formats use opposite terminology with respect to leading + and trailing zeros. The Gerber format specifies which zeros are + suppressed, while the Excellon format specifies which zeros are + included. This function uses the Gerber-file convention, so an + Excellon file in LZ (leading zeros) mode would use + `zero_suppression='trailing'` + + Parameters ---------- value : string A Gerber/Excellon-formatted string representing a numerical value. format : tuple (int,int) - Gerber/Excellon precision format expressed as a tuple containing: + Gerber/Excellon precision format expressed as a tuple containing: (number of integer-part digits, number of decimal-part digits) zero_suppression : string @@ -41,12 +42,12 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): ------- value : float The specified value as a floating-point number. - + """ # Format precision integer_digits, decimal_digits = format MAX_DIGITS = integer_digits + decimal_digits - + # Absolute maximum number of digits supported. This will handle up to # 6:7 format, which is somewhat supported, even though the gerber spec # only allows up to 6:6 @@ -59,40 +60,39 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): negative = '-' in value if negative: value = value.strip(' -') - + # Handle excellon edge case with explicit decimal. "That was easy!" if '.' in value: return float(value) - + digits = [digit for digit in '0' * MAX_DIGITS] offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) for i, digit in enumerate(value): digits[i + offset] = digit - - + result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) return -1.0 * result if negative else result - - + + def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert a floating point number to a Gerber/Excellon-formatted string. - + .. note:: - Format and zero suppression are configurable. Note that the Excellon - and Gerber formats use opposite terminology with respect to leading - and trailing zeros. The Gerber format specifies which zeros are - suppressed, while the Excellon format specifies which zeros are - included. This function uses the Gerber-file convention, so an - Excellon file in LZ (leading zeros) mode would use + Format and zero suppression are configurable. Note that the Excellon + and Gerber formats use opposite terminology with respect to leading + and trailing zeros. The Gerber format specifies which zeros are + suppressed, while the Excellon format specifies which zeros are + included. This function uses the Gerber-file convention, so an + Excellon file in LZ (leading zeros) mode would use `zero_suppression='trailing'` - + Parameters ---------- value : float A floating point value. format : tuple (n=2) - Gerber/Excellon precision format expressed as a tuple containing: + Gerber/Excellon precision format expressed as a tuple containing: (number of integer-part digits, number of decimal-part digits) zero_suppression : string @@ -106,12 +106,12 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): # Format precision integer_digits, decimal_digits = format MAX_DIGITS = integer_digits + decimal_digits - + if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: raise ValueError('Parser only supports precision up to 6:7 format') - + # negative sign affects padding, so deal with it at the end... - negative = value < 0.0 + negative = value < 0.0 if negative: value = -1.0 * value @@ -119,48 +119,72 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits) digits = [val for val in fmtstring % value if val != '.'] - - # Suppression... + + # Suppression... if zero_suppression == 'trailing': while digits[-1] == '0': digits.pop() else: while digits[0] == '0': digits.pop(0) - + return ''.join(digits) if not negative else ''.join(['-'] + digits) - def decimal_string(value, precision=6): """ Convert float to string with limited precision - + Parameters ---------- value : float A floating point value. - precision : + precision : Maximum number of decimal places to print Returns ------- value : string The specified value as a string. - + """ floatstr = '%0.20g' % value integer = None decimal = None if '.' in floatstr: - integer, decimal = floatstr.split('.') + integer, decimal = floatstr.split('.') elif ',' in floatstr: - integer, decimal = floatstr.split(',') + integer, decimal = floatstr.split(',') if len(decimal) > precision: decimal = decimal[:precision] if integer or decimal: return ''.join([integer, '.', decimal]) else: return int(floatstr) - + +def detect_file_format(filename): + """ Determine format of a file + + Parameters + ---------- + filename : string + Filename of the file to read. + + Returns + ------- + format : string + File format. either 'excellon' or 'rs274x' + """ + + # Read the first 20 lines + with open(filename, 'r') as f: + lines = [next(f) for x in xrange(20)] + + # Look for + for line in lines: + if 'M48' in line: + return 'excellon' + elif '%FS' in line: + return'rs274x' + return 'unknown' -- cgit