diff options
-rw-r--r-- | Makefile | 9 | ||||
-rw-r--r-- | gerber/__init__.py | 15 | ||||
-rw-r--r-- | gerber/__main__.py | 10 | ||||
-rwxr-xr-x | gerber/excellon.py | 133 | ||||
-rw-r--r-- | gerber/gerber.py | 77 | ||||
-rw-r--r-- | gerber/render/apertures.py | 9 | ||||
-rw-r--r-- | gerber/render/render.py | 12 | ||||
-rw-r--r-- | gerber/render/svg.py | 50 | ||||
-rw-r--r-- | gerber/statements.py | 187 | ||||
-rw-r--r-- | gerber/utils.py | 108 |
10 files changed, 345 insertions, 265 deletions
@@ -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 (<Tool>, (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 '<Tool %d: %0.3f%s dia.>' % (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 <ham@hamiltonkib.be> # Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com> -# +# # 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, (<float>, <float>) 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: %s>' % 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: %s>' % 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 '<Aperture Definition: %d: %s>' % (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 '<Aperture Macro %s: %s>' % (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 '<Image Name: %s>' % 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 '<Level Name: %s>' % 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 '<Coordinate Statement: %s>' % 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 '<Aperture: %d>' % 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 '<EOF Statement>' - - + + 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 <ham@hamiltonkib.be> -# 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' |